บทความที่เกี่ยวข้อง:
ไดอารี่การพัฒนาสัญญาสมาร์ทสนิม (1) คำจำกัดความของข้อมูลสถานะสัญญาและการนำไปใช้
ไดอารี่การพัฒนาสัญญาสมาร์ทสนิม (2) การเขียนการทดสอบหน่วยสัญญาสมาร์ทสนิม
ไดอารี่การพัฒนาสัญญาสมาร์ทสนิม (1) คำจำกัดความของข้อมูลสถานะสัญญาและการนำไปใช้
ไดอารี่การพัฒนาสัญญาสมาร์ทสนิม (1) คำจำกัดความของข้อมูลสถานะสัญญาและการนำไปใช้
ไดอารี่การพัฒนาสัญญาสมาร์ทสนิม (2) การเขียนการทดสอบหน่วยสัญญาสมาร์ทสนิม
ไดอารี่การพัฒนาสัญญาสมาร์ทสนิม (2) การเขียนการทดสอบหน่วยสัญญาสมาร์ทสนิม
บันทึกการพัฒนาสัญญาอัจฉริยะของสนิม (3) การปรับใช้สัญญาอัจฉริยะของสนิม การเรียกใช้ฟังก์ชัน และการใช้ Explorer
บันทึกการพัฒนาสัญญาอัจฉริยะของสนิม (3) การปรับใช้สัญญาอัจฉริยะของสนิม การเรียกใช้ฟังก์ชัน และการใช้ Explorer
ชื่อระดับแรก
1. ภาพรวมของช่องโหว่ Integer Overflow
รูปภาพ
ในภาษาการเขียนโปรแกรมส่วนใหญ่ ค่าของจำนวนเต็มมักจะถูกเก็บไว้ในหน่วยความจำที่มีความยาวคงที่ จำนวนเต็มสามารถแบ่งออกได้เป็น 2 ประเภท คือแบบไม่มีลายเซ็นและแบบมีลายเซ็น ความแตกต่างระหว่างบิตเหล่านี้คือใช้บิตสูงสุดเป็นบิตเครื่องหมายหรือไม่ ซึ่งใช้เพื่อระบุเครื่องหมายของจำนวนเต็ม ตัวอย่างเช่น พื้นที่หน่วยความจำ 32 บิตสามารถจัดเก็บจำนวนเต็มที่ไม่ได้ลงนาม (uint32) ระหว่าง 0 ถึง 4,294,967,295 หรือจำนวนเต็มที่มีเครื่องหมาย (int32) ระหว่าง −2,147,483,648 ถึง 2,147,483,647
แต่จะเกิดอะไรขึ้นเมื่อเราทำการคำนวณ 4,294,967,295 + 1 ในช่วง uint32 และพยายามเก็บผลลัพธ์ที่มากกว่าค่าสูงสุดของประเภทจำนวนเต็มนั้น
0xFFFFFFFF
+ 0x00000001
------------
= 0x00000000
แม้ว่าผลลัพธ์ของการดำเนินการนี้จะขึ้นอยู่กับภาษาโปรแกรมและคอมไพเลอร์เฉพาะ ในกรณีส่วนใหญ่ ผลลัพธ์ของการคำนวณจะแสดง "ล้น" และคืนค่า 0 ในเวลาเดียวกัน ภาษาการเขียนโปรแกรมและคอมไพเลอร์ส่วนใหญ่ไม่ได้ตรวจสอบข้อผิดพลาดประเภทนี้ แต่ดำเนินการแบบโมดูโลอย่างง่ายเท่านั้น และยังมีพฤติกรรมที่ไม่ได้กำหนดอื่นๆ ด้วย
การมีอยู่ของจำนวนเต็มล้นมักจะทำให้โปรแกรมสร้างผลลัพธ์ที่ไม่คาดคิดในขณะรันไทม์ ในการเขียนสัญญาอัจฉริยะของบล็อกเชน โดยเฉพาะอย่างยิ่งในสาขาการเงินแบบกระจายศูนย์ สถานการณ์การใช้งานของการคำนวณตัวเลขจำนวนเต็มเป็นเรื่องปกติมาก ดังนั้นควรให้ความสนใจเป็นพิเศษกับความเป็นไปได้ของช่องโหว่จำนวนเต็มล้น
0x00000000
- 0x00000001
------------
= 0xFFFFFFFF
สมมติว่าสถาบันการเงินใช้จำนวนเต็ม 32 บิตที่ไม่ได้ลงนามเพื่อแสดงราคาหุ้น อย่างไรก็ตาม เมื่อใช้จำนวนเต็มประเภทนี้เพื่อแสดงจำนวนที่มากกว่าค่าสูงสุดที่ประเภทสามารถแสดงได้ คอมพิวเตอร์จะวางบิตเพิ่มเติม 1 บิตขึ้นไปนอกช่วงหน่วยความจำ 32 บิต (นั่นคือ โอเวอร์โฟลว์) และสุดท้ายคือจำนวน จะแสดงเป็นค่าอื่นนอกเหนือจากโอเวอร์โฟลว์บิตที่ถูกตัดทอน $429,496,7296 จะถูกอ่านเป็น 0 หากเป็นไปได้ ณ จุดนี้ ถ้ามีคนยังคงซื้อขายโดยใช้ค่านั้น ราคาหุ้นจะเป็น 0 ซึ่งจะทำให้เกิดความสับสนทุกรูปแบบ ดังนั้นปัญหาของช่องโหว่จำนวนเต็มล้นสมควรได้รับความสนใจจากเรา
วิธีหลีกเลี่ยงจำนวนเต็มล้นเมื่อเขียนสัญญาอัจฉริยะในภาษา Rust จะเป็นจุดสนใจของการสนทนาในบทความนี้
2. คำจำกัดความจำนวนเต็มล้น
https://etherscan.io/tx/0xad89ff16fd1ebe3a0a7cf4ed282302c06626c1af33221ebe0d3a470aba4a660f
หากค่าเกินช่วงที่ประเภทตัวแปรสามารถแสดงได้ จะส่งผลให้โอเวอร์โฟลว์ โอเวอร์โฟลว์สามารถแบ่งออกเป็นสองกรณีหลักๆ คือ โอเวอร์โฟลว์จำนวนเต็ม (โอเวอร์โฟลว์) และอันเดอร์โฟลว์ (อันเดอร์โฟลว์)
1. function batchTransfer(address[] _receivers, uint256 _value) public whenNotPausedreturns (bool) {
2. uint cnt = _receivers.length;
3. uint256 amount = uint256(cnt) * _value;
4. require(cnt > 0 && cnt <= 20);
5. require(_value > 0 && balances[msg.sender] >= amount);
6.
7. balances[msg.sender] = balances[msg.sender].sub(amount);
8. for (uint i = 0; i < cnt; i++) {
9. balances[_receivers[i]] = balances[_receivers[i]].add(_value);
10. Transfer(msg.sender, _receivers[i], _value);
11. }
12. return true;
13. }
2.1 จำนวนเต็มล้น
นั่นคือ คล้ายกับที่อธิบายไว้ในภาพรวมของช่องโหว่จำนวนเต็มล้นด้านบน ตัวอย่างเช่น ช่วงของจำนวนเต็มที่ไม่มีเครื่องหมายที่สามารถแทนด้วย uint32 ใน Solidity คือ: 0 ถึง 2^32 - 1, 2^32 - 1 จะถูกแทน เนื่องจาก 0xFFFFFFFF เป็นเลขฐานสิบหก 2^32 - 1 บวก 1 จะทำให้ล้น
2.2 จำนวนเต็มอันเดอร์โฟลว์
ช่วงการแสดงของจำนวนเต็มที่ไม่มีเครื่องหมาย uin32 ยังมีขอบเขตล่าง นั่นคือ ค่าต่ำสุดคือ 0 การลบ 1 จาก 0 จะส่งผลให้จำนวนเต็ม uint32 น้อยไป:
3. อินสแตนซ์จำนวนเต็มล้น
ทีมงาน BeautyChain ประกาศเมื่อวันที่ 22 เมษายน 2018 ว่าโทเค็น BEC มีความผันผวนอย่างผิดปกติในวันที่ 22 เมษายน ผู้โจมตีประสบความสำเร็จในการได้รับ 10^58 BECs โดยใช้ประโยชน์จากช่องโหว่ที่เกิดจากจำนวนเต็มล้น
[profile.release]
overflow-checks = true
panic = "abort"
ในเหตุการณ์การโจมตีของสัญญานี้ ผู้โจมตีดำเนินการฟังก์ชัน "batchTransfer" ด้วยช่องโหว่จำนวนเต็มล้นเพื่อทำธุรกรรม
ต่อไปนี้เป็นการใช้งานเฉพาะของฟังก์ชันนี้:
ฟังก์ชันนี้ใช้เพื่อโอนเงินไปยังหลายที่อยู่ (ผู้รับ) และจำนวนเงินที่โอนของแต่ละที่อยู่จะมีค่า
บรรทัดที่สามของโค้ดด้านบน uint256 amount = uint256(cnt) * _value ใช้เพื่อคำนวณจำนวนเงินทั้งหมดที่ต้องโอน แต่มีความเป็นไปได้ที่จำนวนเต็มจะล้นในโค้ดบรรทัดนี้ เมื่อค่า = 0x8000000000000000000000000000000000000000000000000000 และความยาวของตัวรับคือ 2 จำนวนเต็มล้นจะเกิดขึ้นระหว่างการดำเนินการคูณของโค้ดบรรทัดที่สาม ทำให้จำนวน = 0 เนื่องจากจำนวนเงิน = 0 น้อยกว่ายอดคงเหลือของผู้ใช้ [msg.sender] การตรวจสอบว่ายอดคงเหลือของผู้ใช้ที่โทรตามสัญญา msg.sender ในบรรทัดที่ 5 มากกว่าจำนวนเงินที่จะโอนนั้นจะถูกส่งผ่านไปอย่างง่ายดายหรือไม่ ด้วยวิธีนี้ ผู้โจมตีสามารถดำเนินการถ่ายโอนในภายหลังเพื่อทำกำไร
4. เทคโนโลยีการป้องกันจำนวนเต็มล้น
ส่วนนี้จะแนะนำวิธีใช้วิธีการทั่วไปร่วมกับคุณลักษณะของภาษา Rust เพื่อหลีกเลี่ยงจำนวนเต็มล้น
ในภาษา Rust: เมื่อเราคอมไพล์ไฟล์เป้าหมายของเวอร์ชันรีลีส หากไม่ได้กำหนดค่าไว้ Rust จะไม่ตรวจสอบจำนวนเต็มล้นตามค่าเริ่มต้น เมื่อจำนวนเต็มล้น เช่น ในกรณีของจำนวนเต็ม 8 บิตที่ไม่ได้ลงนาม (uint8) วิธีปกติของ Rust คือทำให้ค่า 256 กลายเป็น 0, 257 กลายเป็น 1 ไปเรื่อยๆ ขณะนี้ Rust จะไม่กระตุ้น Panic แต่ค่าของตัวแปรอาจไม่ใช่ค่าที่เราคาดไว้ ดังนั้นเราจึงจำเป็นต้องกำหนดค่าตัวเลือกการคอมไพล์ของโปรแกรม Rust เล็กน้อย เพื่อให้โปรแกรมสามารถตรวจสอบจำนวนเต็มมากเกินไปในโหมด Release และเรียกใช้ Panic เพื่อหลีกเลี่ยงข้อยกเว้นของโปรแกรมที่เกิดจากจำนวนเต็มล้น"0.9.1"กำหนดค่า Cargo.toml เพื่อตรวจสอบจำนวนเต็มล้นในโหมดรีลีส
[dependencies]
เมื่อใช้การกำหนดค่านี้ เราสามารถกำหนดกลยุทธ์การประมวลผลสำหรับจำนวนเต็มล้นในโปรแกรม
uint = { version = "0.9.1", default-features = false }
4.1 ใช้ Rust Crate uint เพื่อรองรับจำนวนเต็มขนาดใหญ่ (ปัจจุบันเวอร์ชันล่าสุดคือ 0.9.1)
use uint::construct_uint;
เมื่อเทียบกับประเภทจำนวนเต็มที่ใหญ่ที่สุดที่ Solidity สามารถรองรับได้คือ u256 ประเภทจำนวนเต็มที่ใหญ่ที่สุดที่ไลบรารีมาตรฐานปัจจุบันของ Rust สามารถให้ได้คือ u128 เท่านั้น เพื่อรองรับการดำเนินการจำนวนเต็มจำนวนมากขึ้นในสัญญาอัจฉริยะของ Rust เราสามารถใช้ Rust uint crate เพื่อช่วยในการปรับขนาดได้
construct_uint! {
pub struct U1024(16);
}
construct_uint! {
pub struct U512(8);
}
construct_uint! {
pub struct U256(4);
}
4.1.1 รู้เบื้องต้นเกี่ยวกับลังสนิม
ใช้ Rust uint crate เพื่อจัดเตรียมประเภทจำนวนเต็มขนาดใหญ่ที่ไม่ได้ลงนาม และการสนับสนุนในตัวสำหรับ API ที่คล้ายกับประเภทจำนวนเต็มดั้งเดิมของ Rust โดยคำนึงถึงประสิทธิภาพและการใช้งานข้ามแพลตฟอร์ม
// (2^1024)-1 = 179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215
let p =U1024::from_dec_str("179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215").expect("p to be a good number in the example");
4.1.2 วิธีใช้ลังสนิมสนิม
#[test]
fn test_uint(){
let p = U1024::from_dec_str("179769313486231590772930519078902473361797697894230657273430081157732675805500963132708477322407536021120113879871393357658789768814416622492847430639474124377767893424865485276302219601246094119453082952085005768838150682342462881473913110540827237163350510684586298239947245938479716304835356329624224137215").expect("p to be a good number in the example");
assert_eq!(p,U1024::max_value());
}
ก่อนอื่นให้เพิ่มการพึ่งพา uint crate ใน Cargo.toml ของโปรเจ็กต์ Rust และระบุหมายเลขเวอร์ชันเป็นล่าสุด
running 1 test
test tests::test_uint ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 3 filtered out; finished in 0.00s
รุ่น.
# การพึ่งพาอื่น ๆ เช่น Near-sdk, Near-contract-standards เป็นต้น
#[test]
fn test_overflow(){
จากนั้นเราสามารถนำเข้าและใช้ลังในโปรแกรม Rust
let amounts: u128 = 340282366920938463463374607431768211455;
คำสั่งต่อไปนี้สามารถใช้เพื่อสร้างประเภทจำนวนเต็มที่ไม่มีเครื่องหมายที่คุณต้องการ:
let amount_u256 = U256::from(amounts) * U256::from(amounts);
println!("{:?}",amount_u256);
4.2 ใช้ฟังก์ชันการแปลงประเภท uint เพื่อตรวจหาจำนวนเต็มล้น
let amount_u256 = U256::from(amounts) + 1;
println!("{:?}",amount_u256);
เราสามารถใช้วิธีต่อไปนี้เพื่อกำหนดตัวแปร p ก่อน และใช้เมธอด from_dec_str ที่กำหนดโดย uint crate สำหรับ U1024 เพื่อกำหนดค่าให้กับตัวแปร p
let amount_u128 = amount_u256.as_u128();
println!("{:?}",amount_u128);
}
การทดสอบหน่วยที่ 1: ใช้เพื่อตรวจสอบว่า uint สามารถรองรับค่าสูงสุดที่ U1024 สามารถแสดงได้หรือไม่
running 1 test
115792089237316195423570985008687907852589419931798687112530834793049593217025
340282366920938463463374607431768211456
thread 'tests::test_overflow' panicked at 'Integer overflow when casting to u128', src/lib.rs:16:1
หน่วยทดสอบหนึ่งผลลัพธ์:
จะเห็นได้ว่าตัวแปร p: U1024 บันทึกค่าสูงสุดที่ U1024 สามารถแสดงได้อย่างแม่นยำ
การทดสอบหน่วยที่ 2: การทดสอบจำนวนเต็มล้น
// ค่าสูงสุดที่ u128 แสดงได้ นั่นคือ 2^128 -1<_>// โดยปกติแล้ว U256 สามารถแสดงผลการทำงานของ (2^128 -1)*(2^128 -1) โดยไม่มีโอเวอร์โฟลว์
// ที่นี่ (2^128 -1) + 1 = 2^128
#[test]
fn test_underflow(){
let amounts= U256::from(0);
let amount_u256 = amounts.checked_sub(U256::from(1));
println!("{:?}",amount_u256);
}
// จะล้นช่วง 0 ถึง 2^128 -1 ที่สามารถแทนด้วย u128 จำนวนเต็มที่ไม่มีเครื่องหมาย ดังนั้น Panic จะถูกกระตุ้น
running 1 test
None
test tests::test_underflow ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 4 filtered out; finished in 0.00s
ผลลัพธ์ของการทดสอบหน่วยมีดังนี้:
#[test]
fn test_underflow(){
let amounts= U256::from(0);
- let amount_u256 = amounts.checked_sub(U256::from(1));
+ let amount_u256 =amounts.checked_sub(U256::from(1)).expect("ERR_SUB_INSUFFICIENT");
println!("{:?}",amount_u256);
}
ตามฟังก์ชันการแปลงประเภท .as_u128() ที่ให้บริการโดย uint crate เมื่อการแปลง amount_u256 เป็น u128 ตามประเภท Painc จะถูกกระตุ้นเพราะมันล้นช่วงที่จำนวนเต็ม u128 ที่ไม่ได้ลงนามสามารถแสดงได้ จะเห็นได้ว่าตอนนี้ Rust สามารถตรวจจับจำนวนเต็มล้นได้
running 1 test
thread 'tests::test_underflow' panicked at 'ERR_SUB_INSUFFICIENT', src/lib.rs:126:62
4.3 การตรวจสอบจำนวนเต็มล้นและอันเดอร์โฟลว์โดยใช้ Safe Math
ภาษาสนิมยังมีลักษณะการทำงานที่แตกต่างกันสำหรับจำนวนเต็มล้นที่อาจเกิดขึ้นในการดำเนินการจำนวนเต็ม หากคุณต้องการควบคุมลักษณะการทำงานของ integer overflow ให้ละเอียดยิ่งขึ้น คุณสามารถเรียกฟังก์ชันชุด wrapping_*, saturating_*,checked_* และ overflow_* ในไลบรารีมาตรฐาน ส่วนนี้จะเน้นไปที่ฟังก์ชันchecked_* ผู้อ่านสามารถค้นหาคีย์เวิร์ดข้างต้นได้ เพื่อเรียนรู้เพิ่มเติม วิธีควบคุมจำนวนเต็มล้น
ประเภทที่ส่งคืนโดย check_* คือตัวเลือก
