คำเตือนความเสี่ยง: ระวังความเสี่ยงจากการระดมทุนที่ผิดกฎหมายในนาม 'สกุลเงินเสมือน' 'บล็อกเชน' — จากห้าหน่วยงานรวมถึงคณะกรรมการกำกับดูแลการธนาคารและการประกันภัย
ข่าวสาร
ค้นพบ
ค้นหา
เข้าสู่ระบบ
简中
繁中
English
日本語
한국어
ภาษาไทย
Tiếng Việt
BTC
ETH
HTX
SOL
BNB
ดูตลาด
Paradigm CTF 2023解题报告
Salus Insights
特邀专栏作者
2024-02-16 10:53
บทความนี้มีประมาณ 10548 คำ การอ่านทั้งหมดใช้เวลาประมาณ 16 นาที
Salus安全团队在Paradigm 2023 CTF中共解决了13项挑战,在1011支队伍中以3645.60的分数获得第九名,并受邀成为Paradigm CTF 2024的客座作者。在这篇博文中,我们将介绍我们在比赛期间解决的所有挑战。

Paradigm CTF เป็นการแข่งขันออนไลน์ชั้นนำและเป็นที่รู้จักมากที่สุดสำหรับแฮกเกอร์สัญญาอัจฉริยะในอุตสาหกรรมบล็อกเชน จัดโดย Paradigm บริษัทการลงทุนชั้นนำใน web3 หัวข้อ CTF ประกอบด้วยความท้าทายหลายประการที่สร้างโดย Sumczsun และผู้แต่งรับเชิญ เป้าหมายของแต่ละความท้าทายคือการแฮ็กหรือโจมตีปัญหาทางเทคนิคเพื่อแก้ไข

ในระหว่างการแข่งขัน ผู้เข้าแข่งขันจะต้องผ่านชุดปริศนาซอฟต์แวร์ คะแนนจะมอบให้สำหรับการท้าทายแต่ละรายการที่ผู้เข้าร่วมแก้ได้ถูกต้องหรือได้รับคะแนนสูงสุดก่อนที่ช่วงการท้าทายจะสิ้นสุดลง สำหรับ King of the Hill Challenge การให้คะแนนจะขึ้นอยู่กับระบบการให้คะแนน Elo จะไม่ทราบจำนวนคะแนนที่ผู้เข้าร่วมแต่ละคนจะได้รับจากการแก้ปัญหาอย่างถูกต้องจนกว่าช่วงระยะเวลาการท้าทายจะสิ้นสุดลง

ทีมรักษาความปลอดภัยของ Salus เอาชนะความท้าทายได้ทั้งหมด 13 ครั้งและจบอันดับที่ 9 จากทั้งหมด 1,011 ทีมด้วยคะแนน 3645.60และได้รับเชิญเป็นนักเขียนรับเชิญในงาน Paradigm CTF 2024ในบล็อกโพสต์นี้ เราจะพูดถึงความท้าทายทั้งหมดที่เราแก้ไขในระหว่างการแข่งขัน

ความท้าทายได้รับการแก้ไขแล้ว

  • Hello World

  • Black Sheep

  • 100% 

  • Dai++

  • DoDont

  • Grains of Sand

  • Suspicious Charity

  • Token Locker

  • Skill Based Game

  • Enterprise Blockchain

  • Dragon Tyrant

  • Hopping Into Place

  • Oven

1. Hello World

เป้าหมายของความท้าทายนี้คือเพื่อให้แน่ใจว่าที่อยู่เป้าหมายมียอดคงเหลือ ETH อย่างน้อย 13.37 มากกว่าเมื่อก่อน

เราสร้างสัญญาสองฉบับ: สัญญาทดสอบ SolveTest และสัญญาเพื่อดำเนินการแก้ไข สัญญา SolveTest จะตรวจสอบว่าความท้าทายได้รับการแก้ไขแล้วโดยการตั้งค่าสภาพแวดล้อมเริ่มต้นและดำเนินการทดสอบการโจมตีสัญญา Solve จะโอนเงินไปยังที่อยู่เป้าหมายผ่านการดำเนินการทำลายตนเองในฟังก์ชัน killMySelf() ดังนั้นจึงบรรลุวัตถุประสงค์ในการเพิ่มความสมดุล ETH ของที่อยู่เป้าหมาย

2. Black Sheep

เป้าหมายของความท้าทายนี้คือการถอน ETH ทั้งหมดออกจากสัญญาของธนาคาร มีช่องโหว่อยู่ในฟังก์ชัน WITHDRAW() เนื่องจากฟังก์ชัน CHECKSIG() ไม่สามารถจัดการค่าที่ส่งคืนได้อย่างถูกต้อง ในบางกรณี ฟังก์ชันจะยุติการดำเนินการโดยตรงโดยไม่ต้องกดค่าใด ๆ ลงในสแต็ก ทำให้ค่าที่ส่งคืนอ่านไม่ถูกต้องเป็น CHECKVALUE( ). ผลลัพธ์ของ. วิธีแก้ปัญหาของเราคือการเขียนสัญญา Solver ที่ใช้ประโยชน์จากช่องโหว่ของฟังก์ชัน WITHDRAW() และทำให้แน่ใจว่า CHECKVALUE() คืนค่า 0 เพื่อให้ฟังก์ชัน WITHDRAW() ดำเนินการได้สำเร็จและแยก ETH ทั้งหมดออกจากสัญญา BANK

การวิเคราะห์ช่องโหว่

เราศึกษาฟังก์ชัน WITHDRAW() ซึ่งจะดำเนินการฟังก์ชัน CHECKVALUE() และ CHECKSIG() ตามลำดับก่อน จากนั้นจึงส่ง ETH ทั้งหมดของสัญญาไปยัง msg.sender ตามผลการดำเนินการ ใน,ฟังก์ชัน CHECKSIG() ไม่สามารถจัดการค่าที่ส่งคืนของฟังก์ชันได้อย่างถูกต้องฟังก์ชันนี้จำเป็นต้องส่งผลลัพธ์ไปยังสแต็กเป็นค่าส่งคืนก่อนที่จะสิ้นสุดการทำงานของฟังก์ชัน อย่างไรก็ตาม ในบางกรณี ฟังก์ชันจะสิ้นสุดการดำเนินการโดยตรงโดยไม่ต้องกดค่าใดๆ ลงในสแต็ก ส่งผลให้ค่าที่ส่งคืนถูกอ่านเป็นองค์ประกอบแรกที่ด้านบนของสแต็กอย่างผิดพลาด ซึ่งเป็นผลการดำเนินการของฟังก์ชัน CHECKVALUE() เนื่องจากข้อบกพร่องด้านการออกแบบในฟังก์ชัน CHECKSIG() แม้ว่าการตรวจสอบลายเซ็นจะล้มเหลว ฟังก์ชัน WITHDRAW() ก็สามารถทำได้สำเร็จโดยทำให้แน่ใจว่าฟังก์ชัน CHECKVALUE() ส่งคืนค่า 0

ในฟังก์ชัน CHECKSIG() ให้เรียกใช้ฟังก์ชัน WITHDRAW() โดยใช้พารามิเตอร์อินพุต (ไบต์ 32, uint 8, ไบต์ 32, ไบต์ 32) เพื่อเรียกที่อยู่ 0x 1 สัญญานี้เป็นสัญญาที่คอมไพล์แล้วซึ่งมีหน้าที่ในการกู้คืนที่อยู่คีย์สาธารณะตามพารามิเตอร์ มีการตรวจสอบสองรายการที่นี่ ประการแรกคือการตรวจสอบว่าลายเซ็นนั้นถูกต้องหรือไม่ หาก staticcall ดำเนินการสำเร็จ แสดงว่าลายเซ็นนั้นถูกต้อง ดังนั้นเนื้อหาของพารามิเตอร์อินพุตจึงไม่สำคัญ ตรวจสอบความถูกต้องของรหัสสาธารณะในครั้งที่สอง หากที่อยู่คีย์สาธารณะไม่ถูกต้อง ที่อยู่จะไม่ถอยกลับ แต่จะข้ามไปที่จุดสิ้นสุดของฟังก์ชันโดยตรง ฟังก์ชันนี้มีค่าส่งคืน และตามการดำเนินการปกติ ผลลัพธ์จะต้องถูกผลักลงบนสแต็กเป็นค่าส่งคืนก่อนที่จะสิ้นสุดการทำงานของฟังก์ชัน

อย่างไรก็ตาม หากการดำเนินการสิ้นสุดลงโดยตรง จะไม่มีการพุชค่าใด ๆ ลงบนสแต็ก ซึ่งจะทำให้ค่าที่ส่งคืนอ่านไม่ถูกต้องเป็นองค์ประกอบแรกที่ด้านบนของสแต็ก ซึ่งเป็นผลลัพธ์ของ CHECKVALUE()ดังนั้น ตราบใดที่ผลลัพธ์การดำเนินการของฟังก์ชัน CHECKVALUE() ส่งคืนค่า 0 ฟังก์ชัน WITHDRAW() ก็สามารถดำเนินการได้อย่างราบรื่นและส่ง 10 ETH ไปยัง msg.sender ได้สำเร็จ

เราหวังว่าผลการดำเนินการของฟังก์ชัน CHECKVALUE() จะเป็น 0 นั่นคือองค์ประกอบด้านบนของสแต็กคือ 0 เราเพียงต้องตอบสนอง 0x 10 > ค่าการโทร เท่านั้นเพื่อทำให้การดำเนินการโทรล้มเหลว

สารละลาย

เราเขียนสัญญา Solver เพื่อถอนเงินจากสัญญาของธนาคาร ETH ในสัญญาของธนาคารจะถูกส่งไปยังสัญญา Solver ผ่านการดำเนินการเรียกในฟังก์ชัน WITHDRAW() กระบวนการเฉพาะมีดังนี้:

  1. ในฟังก์ชัน solve() ในสัญญา Solver ให้เรียกใช้ฟังก์ชัน WITHDRAW() ของสัญญาธนาคารเพื่อเริ่มการดำเนินการถอนเงิน

  2. ในฟังก์ชัน WITHDRAW() ฟังก์ชัน CHECKVALUE() จะถูกดำเนินการก่อนเนื่องจากค่าการโทรของเราคือ 5 wei (น้อยกว่า 0x 10) มันจะข้ามไปที่ป้ายกำกับโอเวอร์

  3. ในแท็กโอเวอร์ ค่าการโทร * 2 (นั่นคือ 10 wei) จะถูกส่งไปยังผู้เรียก (นั่นคือ สัญญา Solver) เนื่องจากในฟังก์ชันสำรองของสัญญา Solverหากจำนวนอีเธอร์ที่ได้รับเท่ากับ 10 wei ธุรกรรมจะถูกย้อนกลับ ดังนั้นการดำเนินการเรียกในแท็กโอเวอร์จะล้มเหลว และฟังก์ชัน CHECKVALUE() ส่งคืน 0

  4. ฟังก์ชัน WITHDRAW() ยังคงดำเนินการต่อไปส่งยอดคงเหลือทั้งหมดของสัญญาธนาคารไปยังผู้โทร (นั่นคือ สัญญา Solver) นี่คือความสำเร็จผ่านบรรทัดของรหัส selfbalance ผู้โทรเรียกแก๊สในหมู่พวกเขา ความสมดุลในตัวเองคือความสมดุลของสัญญา ผู้โทรคือที่อยู่ของผู้โทร และการโทรแบบแก๊สคือการดำเนินการที่เริ่มต้นการโทร

  5. หากการดำเนินการเรียกนี้สำเร็จ ยอดคงเหลือทั้งหมดของสัญญาธนาคารจะถูกส่งไปยังสัญญา Solver หากการดำเนินการนี้ล้มเหลว ระบบจะข้ามไปยังป้ายกำกับ noauth โดยตรงและดำเนินการเปลี่ยนกลับเพื่อย้อนกลับธุรกรรม

3. 100% 

เป้าหมายของความท้าทายนี้คือยอดคงเหลือ ETH ของทั้ง SPLIT และ _splitsById[ 0 ].wallet จะต้องเป็น 0 มีช่องโหว่อยู่ในฟังก์ชัน distribution() ซึ่งจะตรวจสอบพารามิเตอร์โดยการเปรียบเทียบแฮชของผลลัพธ์ abi.encodePacked เท่านั้น แต่เนื่องจากบัญชีและเปอร์เซ็นต์ถูกพิมพ์แบบไดนามิก จึงสามารถปรับได้ในระหว่างกระบวนการจัดสรร วิธีแก้ปัญหาของเราคือการดึง ETH มากกว่าที่เราฝากโดยการจัดการบัญชีและอาร์เรย์เปอร์เซ็นต์ โดยใช้ประโยชน์จากการตรวจสอบอาร์กิวเมนต์ที่ไม่เพียงพอในฟังก์ชัน distribution()

การวิเคราะห์ช่องโหว่

ฟังก์ชัน distribution() ของสัญญาแยกสามารถใช้เพื่อกระจายสินทรัพย์เฉพาะตามบัญชีและเปอร์เซ็นต์ที่ระบุเมื่อสร้าง SplitWallet หลังจากการจัดสรรผู้ใช้สามารถถอนเงินตามมูลค่าที่เก็บไว้ในยอดคงเหลืออย่างไรก็ตาม ฟังก์ชัน distribution() ประสบปัญหาจากการตรวจสอบความถูกต้องของพารามิเตอร์ไม่เพียงพอ ฟังก์ชันนี้จะตรวจสอบพารามิเตอร์โดยการเปรียบเทียบแฮชของผลลัพธ์ abi.encodePacked เท่านั้น ในขณะที่บัญชีและเปอร์เซ็นต์ถูกพิมพ์แบบไดนามิก ดังนั้นในระหว่างกระบวนการจัดสรร เราสามารถปรับบัญชีและเปอร์เซ็นต์ได้เล็กน้อย

เมื่อสร้าง SplitWallet{id: 0} บัญชีสำหรับดัชนีแรกถูกเว้นว่างไว้โดยไม่ตั้งใจ

ดังนั้นเราจึงสามารถแยก ETH ทั้งหมดจาก SplitWallet{id: 0} โดยใช้บัญชีที่แก้ไขและเปอร์เซ็นต์ แต่ไม่สามารถแจกจ่ายให้กับใครก็ได้ ในขณะที่ยังคงรักษาแฮชไว้ไม่เปลี่ยนแปลง (โปรดทราบว่าองค์ประกอบอาร์เรย์ถูกเสริมเป็น 32 ไบต์)

ในทำนองเดียวกัน เราสามารถใช้การชนกันของแฮชที่เกิดจาก abi.encodePacked เพื่อถอน ETH มากกว่าที่ฝากเพื่อระบายการแยก

สารละลาย

เราเขียนฟังก์ชันการแก้ปัญหาเป็นหลักเพื่อล้างยอดคงเหลือ ETH ของ SPLIT และ _splitsById[ 0 ].walletกุญแจสำคัญในการแก้ปัญหาทั้งหมดคือการดึง ETH ให้มากขึ้นโดยไม่ละเมิดกลไกการตรวจสอบแฮชโดยการจัดการบัญชีและอาร์เรย์เปอร์เซ็นต์ และใช้ประโยชน์จากพฤติกรรมของฟังก์ชันการแจกจ่ายแนวคิดเฉพาะมีดังนี้:

  1. ด้วยการปรับบัญชีและอาร์เรย์เปอร์เซ็นต์ คุณสามารถควบคุมการจัดสรร ETH ได้ ที่นี่เราใช้อาร์เรย์บัญชีที่มีที่อยู่เดียวเท่านั้น (ที่อยู่ของเรา) และอาร์เรย์เปอร์เซ็นต์ที่มีสององค์ประกอบ

  2. ใช้ฟังก์ชัน split.distribute เพื่อถอน ETH จาก SplitWallet ไปยังบัญชีของเรา ขั้นตอนนี้ทำได้โดยการปรับพารามิเตอร์ในฟังก์ชันการกระจายอย่างเหมาะสมเพื่อให้แน่ใจว่าเราจะได้รับ ETH

  3. จากนั้น สร้างอินสแตนซ์ Split และตั้งค่าที่อยู่ของเราเป็นผู้รับ

  4. ฝาก ETH จำนวนหนึ่งผ่านฟังก์ชัน split.deposit จากนั้นใช้ฟังก์ชัน split.distribute อีกครั้งเพื่อถอน ETH เพิ่มเติม

  5. สุดท้าย เรียกใช้ฟังก์ชัน split.withdraw เพื่อถอน ETH ทั้งหมดออกจากสัญญา Split เพื่อดำเนินการท้าทายให้เสร็จสิ้น

4. Dai++

เป้าหมายของความท้าทายนี้คือการได้รับอุปทานรวมของ Stablecoin ให้เกิน 10^12* 10^18 ช่องโหว่คือเมื่อสัญญา AccountManager ใช้ ClonesWithImmutableArgs เพื่อสร้างบัญชีใหม่ ขีดจำกัดความยาวของพารามิเตอร์ที่ไม่เปลี่ยนรูปจะถูกละเว้น ส่งผลให้สัญญาเสียหายถูกปรับใช้เมื่อความยาวพารามิเตอร์เกิน 65535 ไบต์ วิธีแก้ปัญหาของเราคือการสร้างบัญชีที่มีพารามิเตอร์ที่ยาวเกินไป โดยเปลี่ยนฟังก์ชัน เพิ่ม Debt() ให้เป็นฟังก์ชันแฝง ดังนั้นจึงข้ามการตรวจสุขภาพและอนุญาตให้สร้างเหรียญ stablecoin จำนวนมากได้โดยไม่ต้องเพิ่มหนี้

การวิเคราะห์ช่องโหว่

เฉพาะบัญชีที่ได้รับอนุญาตจากสัญญา SystemConfiguration เท่านั้นที่สามารถสร้างเหรียญ stablecoin ได้ เฉพาะเจ้าของ SystemConfiguration เท่านั้นที่สามารถอัพเดตสัญญาระบบ (นั่นคือ อนุญาตบัญชี) และสัญญา AccountManager เป็นสัญญาที่ได้รับอนุญาตเพียงสัญญาเดียว

ในสัญญา AccountManager เฉพาะบัญชีที่ถูกต้องเท่านั้นที่สามารถสร้างเหรียญ stablecoin ได้ ในขณะเดียวกันหนี้ในบัญชีก็จะเพิ่มขึ้นด้วย

ในฟังก์ชัน เพิ่มหนี้() หากบัญชีไม่แข็งแรงหลังจากหนี้เพิ่มขึ้น ธุรกรรมจะล้มเหลว อย่างไรก็ตาม ผู้เล่นไม่มี ETH เพียงพอที่จะสร้างเหรียญเสถียร 10^12 เหรียญและทำให้บัญชีแข็งแรง

เป็นที่น่าสังเกตว่า AccountManager ใช้ ClonesWithImmutableArgs เพื่อสร้างบัญชีใหม่ เมื่อโต้ตอบกับบัญชี พารามิเตอร์ที่ไม่เปลี่ยนรูปจะถูกอ่านจากข้อมูลการโทรเพื่อประหยัดค่าน้ำมัน แต่มีความคิดเห็นใน ClonesWithImmutableArgs: @dev data ไม่เกิน 65535 ไบต์เนื่องจาก 2 ไบต์ใช้เพื่อจัดเก็บความยาวของข้อมูล

เนื่องจากพารามิเตอร์ที่ไม่เปลี่ยนรูปจะถูกจัดเก็บไว้ในพื้นที่โค้ดของสัญญาพร็อกซีที่สร้างขึ้น ในระหว่างการปรับใช้ ขนาดโค้ดจะถูกคำนวณตามความยาวของข้อมูล อย่างไรก็ตาม ขนาดโค้ดที่ควรส่งคืนจะถูกเก็บไว้ในขนาด 2 ไบต์เช่นกัน ดังนั้น,หาก runSize เกิน 65535 ไบต์ สัญญาที่ใช้งานไม่ได้อาจถูกปรับใช้เราสามารถถือว่าฟังก์ชัน addDebt() เป็นฟังก์ชัน Phantom เพื่อเพิกเฉยต่อการเรียกนี้

ความยาวพารามิเตอร์ที่มีอยู่คือ 20 + 20 + 32 = 72 ไบต์ ความยาวของที่อยู่การกู้คืนที่เข้ารหัสจะเป็นผลคูณของ 32 ไบต์

สารละลาย

เราเขียนสัญญา Solve และใช้ประโยชน์จากช่องโหว่ของสัญญา AccountManager เพื่อสร้างเหรียญที่มีเสถียรภาพจำนวนมาก

  1. ขั้นแรก สร้างบัญชีใหม่ที่มีพารามิเตอร์ที่ยาวผิดปกติโดยเรียกใช้ฟังก์ชัน openAccount ของ AccountManager ซึ่งสามารถทำได้โดยการส่งอาร์เรย์ที่อยู่ว่างที่มีความยาว 2044 ซึ่งทำให้สัญญาพร็อกซีที่สร้างขึ้นภายในเสียหายเนื่องจากความยาวของพารามิเตอร์เกินขีดจำกัด 65535 ไบต์ที่คาดไว้

  2. เพื่อให้แน่ใจว่าความยาวของพารามิเตอร์ถูกต้อง จะใช้สูตรการคำนวณ 72 + 2044 * 32 + 2 + 0x 43 - 11 = 65538 ที่นี่ 72 คือความยาวพารามิเตอร์ที่มีอยู่ 2044 * 32 คือความยาวที่เข้ารหัสของ recoveryAddresses 2 คือจำนวนไบต์ที่จะจัดเก็บความยาวข้อมูล 0x 43 คือความยาวไบต์โค้ดในขั้นตอนการสร้าง 11 คือความยาวไบต์โค้ดเมื่อสัญญารันไทม์ ถูกสร้างขึ้น ผลการคำนวณ 65538 เกินความยาวสูงสุด 65535 ดังนั้นสัญญาที่ใช้งานไม่ได้จะถูกสร้างขึ้นในการปรับใช้

  3. ใช้บัญชีที่เสียหายที่สร้างขึ้นใหม่เพื่อสร้างเหรียญ stablecoin จำนวนมากผ่านฟังก์ชัน mintStablecoins เนื่องจากความเสียหายต่อสัญญาบัญชีฟังก์ชั่นเพิ่มหนี้ (ซึ่งควรเพิ่มหนี้ในบัญชี) จะไม่สามารถดำเนินการได้อย่างถูกต้องจริง ทำให้สามารถผลิตเหรียญ stablecoin ได้โดยไม่ต้องเพิ่มหนี้ใดๆ

5. DoDont

เป้าหมายของความท้าทายนี้คือการขโมย WETH ทั้งหมดในโครงการ DVM (กลไกการลงคะแนนเสียงพร็อกซี) ช่องโหว่นี้อยู่ในฟังก์ชันเริ่มต้นของ DVM.sol ซึ่งไม่มีข้อจำกัดในการโทร ทำให้ใครก็ตามสามารถเปลี่ยนที่อยู่ BASE_TOKEN และ QUOTE_TOKEN ได้ โซลูชันของเราจะใช้ประโยชน์จากช่องโหว่นี้โดยการเปลี่ยนที่อยู่เหล่านี้เป็นสัญญาโทเค็นที่เราควบคุมในระหว่างกระบวนการยืมแฟลช โดยข้ามการตรวจสอบยอดคงเหลือของกลไกการยืมแฟลช

การวิเคราะห์ช่องโหว่

หลังจากการทบทวนโครงการ DVM นี้อย่างรวดเร็ว เราก็สังเกตเห็นฟังก์ชัน init ใน DVM.sol ไม่มีข้อจำกัดในการโทรนี่คือต้นตอของปัญหา

เราสามารถเรียกใช้ฟังก์ชัน init() ได้ตลอดเวลาเพื่อเปลี่ยน BASE_TOKEN และ QUOTE_TOKENเหล่านี้คือที่อยู่โทเค็นพื้นฐานของการกู้ยืมแฟลชในการท้าทาย การใช้ประโยชน์จากช่องโหว่ดังกล่าวในสินเชื่อแฟลชเป็นเรื่องง่ายเพราะเราต้องการในระหว่างกระบวนการยืมแฟลช ให้เปลี่ยน BASE_TOKEN และ QUOTE_TOKEN เป็นที่อยู่สัญญาโทเค็นที่พวกเขาควบคุม ซึ่งช่วยให้สามารถควบคุมยอดคงเหลือในระหว่างระยะเวลายืมแฟลช โดยข้ามการตรวจสอบยอดคงเหลือในกลไกการยืมแฟลช

สารละลาย

เราสร้างสัญญา Solve เพื่อโต้ตอบกับสัญญาท้าทาย สร้างสัญญา Exploit เพื่อทำการโจมตี ขั้นแรกสัญญาจะใช้ฟังก์ชัน flashLoan เพื่อรับยอดคงเหลือ WETH จากนั้นเรียก init ผ่านฟังก์ชัน DVMFlashLoanCall เพื่อเปลี่ยนที่อยู่ของ BASE_TOKEN และ QUOTE_TOKEN เป็นสัญญาโทเค็นที่ควบคุม ด้วยวิธีนี้ เราสามารถข้ามการตรวจสอบยอดคงเหลือของกลไกการยืมแฟลช และขโมย WETH ทั้งหมดใน DVM ได้ในที่สุด

6.Grains of Sand

เป้าหมายของความท้าทายนี้คือการลดยอดคงเหลือ GoldReserve (XGR) ในร้านค้าโทเค็นอย่างน้อย 11111 × 10^8 ช่องโหว่คือโทเค็น GoldReserve จะถูกเรียกเก็บค่าธรรมเนียมเมื่อทำการโอน แต่ร้านโทเค็นไม่รองรับโทเค็นที่มีค่าธรรมเนียมการโอน วิธีแก้ปัญหาของเราคือการระบายโทเค็นที่จัดเก็บโดยการฝากและถอนโทเค็น GoldReserve ซ้ำ ๆ (การดำเนินการทั้งสองมีค่าธรรมเนียมการโอน)

การวิเคราะห์ช่องโหว่

ห่วงโซ่ส่วนตัวที่มีความท้าทายนี้ถูกแยกออกจากบล็อก 18437825 ของ Ethereum mainnet

โทเค็น GoldReserve (XGR) จะต้องเสียค่าธรรมเนียมเมื่อทำการโอน แต่โทเค็นที่มีค่าธรรมเนียมการโอนไม่ได้รับการสนับสนุนโดย Token Store ดังนั้นเราจึงสามารถระบายเหรียญออกจากร้านค้าได้โดยการฝากและถอนเงินซ้ำๆ

ตอนนี้เราต้องได้รับโทเค็น GoldReserve ก่อน! ด้วยฟังก์ชัน trade() เราสามารถแลกเปลี่ยนลายเซ็นเป็น $XGR ได้

สามารถกรอกคำสั่งซื้อขายได้บางส่วน ผ่านDuneเราสามารถค้นหาคำสั่งซื้อโทเค็น GoldReserve ที่ยังไม่หมดอายุได้ โชคดีที่มีคำสั่งซื้อสองรายการที่มีเหรียญที่ขายไม่ออกจำนวนมาก

สารละลาย

เราสร้างสัญญา Solve เพื่อโต้ตอบกับสัญญาท้าทาย ขั้นแรก แลกเปลี่ยนเพื่อรับโทเค็น GoldReserve ผ่านฟังก์ชัน trade() จากนั้นใช้กลไกการฝากและถอนในร้านค้าโทเค็นเพื่อดำเนินการซ้ำๆ เพื่อลดยอดโทเค็นในร้านค้าโทเค็น ด้วยวิธีนี้ โทเค็น GoldReserve จึงสามารถระบายออกจากร้านค้าโทเค็นได้สำเร็จ โดยเป็นไปตามเงื่อนไขของความท้าทาย

7.Suspicious Charity

เป้าหมายของความท้าทายนี้คือการจัดการแคชราคาในสคริปต์ Python เพื่อส่งผลต่อการคำนวณราคาและสภาพคล่องของโทเค็น ช่องโหว่ในความท้าทายนี้เกิดขึ้นจากที่อยู่โทเค็นแคชของสคริปต์ Python ในพูลตามชื่อ เมื่อชื่อเหล่านี้ถูกสร้างขึ้นโดยใช้สตริง (uint 8) ค่าที่สูงกว่า 0x 80 จะเหมือนกันใน Python นำไปสู่การแคชที่ไม่ถูกต้อง โซลูชันของเราคือการสร้างคู่การซื้อขายสองคู่: คู่หนึ่งคือคู่การซื้อขายที่มีราคาสูงและสภาพคล่องต่ำ ซึ่งใช้ในการอัปเดต tokenPrice ในแคช ส่วนอีกคู่คือคู่การซื้อขายที่มีราคาต่ำและมีสภาพคล่องสูง ซึ่งได้รับการอัปเดตใน tokenAmount พูลชื่อเดียวกัน ด้วยวิธีการนี้ โดยใช้การคำนวณผิดในสคริปต์ Python เราจึงสามารถจัดการราคาโทเค็นและสภาพคล่องได้สำเร็จ และในที่สุดก็บรรลุเป้าหมายในการขโมย WETH ทั้งหมดใน DVM

การวิเคราะห์ช่องโหว่

ปัญหาเกิดจากที่อยู่โทเค็นแคชสคริปต์ Python ในพูลตามชื่อ ซึ่งสร้างขึ้นโดยใช้สตริง (uint 8) เราได้สังเกตเห็นว่าเมื่อค่าเกิน 0x80 ค่าเหล่านั้นจะเหมือนกันในสคริปต์ Python ซึ่งอาจนำไปสู่การแคชที่ไม่ถูกต้อง ในฟังก์ชัน get_pair_prices ของสคริปต์ Python ส่งผลให้การคำนวณราคาไม่ถูกต้อง

อันดับแรกเราสร้างคู่การซื้อขายที่ไร้ประโยชน์ 78 คู่ จากนั้นจึงสร้างคู่การซื้อขายที่ถูกบิดเบือนสองคู่เพื่อเริ่มการโจมตี

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

สารละลาย

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

8.Token Locker

เป้าหมายของความท้าทายนี้คือการใช้ประโยชน์จากช่องโหว่ของสัญญา UNCX_ProofOfReservesV2_UniV3 เพื่อขโมย NFT ในสัญญา ช่องโหว่คือฟังก์ชัน lock() ช่วยให้ผู้ใช้สามารถล็อกสภาพคล่องในสัญญาได้ แต่พารามิเตอร์ nftPositionManager ในโครงสร้าง LockParams ที่ได้รับจากฟังก์ชันสามารถถูกแทนที่ด้วยสัญญาที่เป็นอันตรายได้ สิ่งนี้ช่วยให้เราสามารถควบคุมตำแหน่งและสภาพคล่องของ NFT ผ่านตัวจัดการตำแหน่ง NFT แบบกำหนดเอง โซลูชันของเราคือการสร้างสัญญา TokenLockerExploit ซึ่งดำเนินการฟังก์ชันล็อคในสัญญา UNCX_ProofOfReservesV2_UniV3 และใช้สัญญา CustomNftPositionManager เพื่อจัดการตำแหน่งและสภาพคล่องของ NFT ด้วยวิธีนี้ เราสามารถโอนและควบคุมสินทรัพย์ในสัญญา NFT และล้างเงินทุนในสัญญาได้สำเร็จในที่สุด

การวิเคราะห์ช่องโหว่

ปัญหานี้เกิดขึ้นจากสัญญา UNCX_ProofOfReservesV2_UniV3 ซึ่งจริงๆ แล้วเป็นส่วนแยกของสัญญา 0x7f5C649856F900d15C83741f45AE46f5C6858234 บน Ethereum mainnet หลังจากตรวจสอบโค้ดอย่างรวดเร็วแล้ว เราจำเป็นต้องพิจารณาฟังก์ชันภายนอกที่ผู้ใช้สามารถโต้ตอบด้วยให้ละเอียดยิ่งขึ้น โดยเฉพาะฟังก์ชัน lock()

ในสัญญา UNCX_ProofOfReservesV2_UniV3 ฟังก์ชัน lock() ช่วยให้ผู้ใช้สามารถปกป้องสภาพคล่องของตนได้โดยการล็อกไว้ในสัญญา ฟังก์ชันนี้มีสองตัวเลือก: ผู้ใช้สามารถแปลง NFT เป็นขอบเขตทั้งหมดและเรียกร้องค่าธรรมเนียมที่เกี่ยวข้อง ซึ่งจะถูกส่งกลับไปยังผู้ขอ หรือพวกเขาสามารถใช้ประโยชน์จากตำแหน่งที่มีอยู่แล้วได้

ฟังก์ชันนี้รับโครงสร้าง LockParams เป็นพารามิเตอร์อินพุต โดยเฉพาะ nftPositionManager

INonfungiblePositionManager ความพร้อมใช้งานของ nftPositionManager หมายความว่าเราสามารถเข้าสู่สัญญาของเราได้ ซึ่งจะส่งคืน UNCX_ProofOfReservesV2_UniV3 จากการเรียกภายนอกที่จำเป็นต้องระบายสัญญา

ในระหว่างการดำเนินการของฟังก์ชัน lock() ฟังก์ชัน _convertPositionToFullRange() อาจถูกเรียก ด้านล่างนี้คือจุดอ่อน

เราแค่ต้องส่งพารามิเตอร์ดังนี้:

  1. mintParams.token 0 // nftPositionManager ส่งคืนที่อยู่ของผู้จัดการตำแหน่ง Uniswap จริง

  2. ที่อยู่ (_nftPositionManager) // ปรับแต่งที่อยู่ของ nftPositionManager

  3. mintParams.amount 1 Desired // เราควรส่ง NFT ID ที่เราต้องการระบาย

เนื่องจาก ERC 721 และ ERC 20 มีฟังก์ชัน Transfer() เหมือนกัน นิพจน์ต่อไปนี้ในฟังก์ชัน _convertPositionToFullRange() จะส่งผลให้ถ่ายโอน NFT ของตัวเองไปยัง nftPositionManager ที่เป็นอันตราย:

สารละลาย

เราสร้างสัญญา TokenLockerExploit เพื่อขโมย NFT สัญญานี้ตระหนักถึงการล้างเงินทุนของสัญญาโดยการจัดการฟังก์ชัน lock() ในสัญญา UNCX_ProofOfReservesV2_UniV3 และจัดการตำแหน่งและสภาพคล่องของ NFT ผ่านสัญญา CustomNftPositionManager

9. Skill Based Game

เป้าหมายของความท้าทายนี้คือการใช้เงินทุนทั้งหมดใน 0xA65D59708838581520511d98fB8b5d1F76A96cad Ethereum mainnet โดยการชนะเกม BlackJack ติดต่อกัน จุดอ่อนของความท้าทายนี้คือฟังก์ชันการซื้อขายสัญญาเกม BlackJack (Deck.deal()) อาศัยคุณลักษณะของบล็อก (เช่น block.number และ block.timestamp) เพื่อจำลองการสุ่ม ซึ่งอาจทำให้สามารถคาดเดาผลลัพธ์ได้ แนวทางแก้ไขของเราคือการสร้างสัญญาโจมตีเพื่อจำลองกระบวนการแจกไพ่และตัดสินใจว่าจะทำการเดิมพันจริงตามผลลัพธ์ที่คาดการณ์ไว้หรือไม่

การวิเคราะห์ช่องโหว่

เพื่อที่จะบรรลุความท้าทายนี้ เราจำเป็นต้องรู้ล่วงหน้าว่าไพ่ใดจะถูกจั่ว เพื่อที่เราจะได้มีข้อมูลในการตัดสินใจว่าจะเล่นเกมไหน ตอนนี้เรามาดูกันว่าสัญญาควบคุมการแจกไพ่อย่างไร ผู้เล่นจำเป็นต้องเรียกใช้ฟังก์ชัน deal() และ checkGameResult() จะต้องถูกทริกเกอร์ในตอนท้าย:

กระบวนการซื้อขายได้รับการจัดการภายในฟังก์ชัน Deck.deal()วิธีการสร้างการสุ่มนี้ขึ้นอยู่กับคุณสมบัติของบล็อกและตัวแปรบางตัว,ตามที่แสดงโดยข้อมูลโค้ดด้านล่างการใช้งานนี้ทำให้เกิดช่องโหว่ที่ทำให้สามารถคาดการณ์ผลลัพธ์ได้

กระบวนการแจกไพ่เกี่ยวข้องกับการคำนวณบล็อคแฮช ที่อยู่ของผู้เล่น จำนวนไพ่ที่แจก และแฮชของ block.timestamp นี่เป็นวิธีที่รู้จักกันดีในการเลียนแบบการสุ่ม เพียงแค่รอบล็อคที่ต้องการ คำนวณผลลัพธ์ของเกมใหม่ตามข้อมูลใหม่ และหากผลลัพธ์ของเกมตรงกับข้อกำหนดของเรา เราก็จะต้องเล่น

สารละลาย

เราสร้างสัญญาผู้โจมตีโดยใช้ไลบรารี Deck เพื่อทำการโจมตี สัญญาจะจำลองกระบวนการแจกไพ่ก่อน จากนั้นจึงตัดสินใจว่าจะทำการเดิมพันจริงตามผลที่คาดการณ์ไว้หรือไม่

ณ จุดนี้ เราเพียงแต่ต้องดำเนินการฟังก์ชัน play() ในสัญญานี้ซ้ำๆ เท่านั้น โดยใช้ค่า 5 อีเธอร์ จนกว่าเงินทุนในสัญญา BLACKJACK จะหมด นี่คือสคริปต์เพื่อให้บรรลุเป้าหมายนี้:

10. Enterprise Blockchain

เป้าหมายของความท้าทายนี้คือการดึง FlagToken อย่างน้อย 10 อันจากสะพาน l1 บนห่วงโซ่ L1 ช่องโหว่ของความท้าทายคือโหนด L2 อาจหยุดทำงานเมื่อประมวลผลการเรียกสัญญาที่คอมไพล์แล้วของ ADMIN เฉพาะ ทำให้โหนด L2 รีสตาร์ทและโหลดจากสถานะก่อนหน้า วิธีแก้ปัญหาของเราคือการใช้ประโยชน์จากช่องโหว่นี้และส่งข้อความระยะไกลจาก L2 ไปยัง L1 ก่อนเพื่อถ่ายโอน FlagTokens ไปยัง L1 จากนั้นทริกเกอร์โหนด L2 ให้หยุดทำงานและรีสตาร์ท ด้วยวิธีนี้ แม้ว่าสถานะของโหนด L2 จะถูกกู้คืนก่อนที่จะมีการโอนเกิดขึ้น เงินก็ถูกโอนไปยัง L1 ได้สำเร็จ และเงินทุนใน L2 ยังไม่ลดลง จึงบรรลุเป้าหมายของความท้าทาย

การวิเคราะห์ช่องโหว่

มีสองโซ่ที่นี่

(1) สัญญาท้าทายจะใช้งานบน L1เริ่มแรกมี 100 FlagTokens (ทศนิยม 18 ตำแหน่ง) ใน l1 Bridge

ผู้ใช้สามารถใช้สะพานเพื่อโอนเงินระหว่างเครือข่ายได้ รีเลย์จะฟังเหตุการณ์ SendRemoteMessage บนเชนทั้งสองและส่งต่อข้อความไปยังเชนเป้าหมาย

เพื่อที่จะออกเหตุการณ์ SendRemoteMessage เราสามารถเรียกใช้ฟังก์ชัน sendRemoteMessage() และธุรกรรมที่จะดำเนินการในห่วงโซ่อื่นสามารถปรับแต่งได้

เนื่องจากมีการจัดหา L2 RPC ไว้ด้วยและผู้เล่นเป็นเจ้าของอีเธอร์ เราจึงสามารถส่งข้อความระยะไกลจาก L2 ไปยัง L1 และถ่ายโอนโทเค็นจากสะพาน l1 ไปยังผู้ใช้ได้

แต่,the sendRemoteMessage() ฟังก์ชั่นนี้ไม่ได้มีไว้สำหรับการใช้งานสาธารณะ แต่คาดว่าจะโอนเงินระหว่างเครือข่ายผ่าน ethOut() / ERC 20 Out() เท่านั้น

(2) สัญญา SimpleMultiSigGov ถูกปรับใช้บนห่วงโซ่ L2ซึ่งตั้งอยู่ตามที่อยู่ 0x 31337 สามารถใช้เพื่อโต้ตอบกับ ADMIN สัญญาที่คอมไพล์แล้ว

สัญญาที่คอมไพล์แล้วของ ADMIN มีฟังก์ชัน fn_dump_state() ซึ่งเป็นการดำเนินการภายในที่อาจนำไปสู่พฤติกรรมที่ไม่ได้กำหนดไว้ ขั้นแรก x.len() ควรมากกว่า 0x 10 มิฉะนั้นโปรแกรมจะตื่นตระหนกเนื่องจากดัชนีนอกขอบเขตเมื่อ i == x.len() state เป็นตัวชี้ดิบไปยังสไลซ์ [u 8 ] ซึ่งมีขนาด 16 ไบต์บน x 86-64 หน่วยการนับของ state.offset คือการแบ่งส่วน เนื่องจากค่าสูงสุดของ i คือ 0x 10 หน่วยความจำขั้นต่ำที่ควรจัดสรรคือ 0x 110 (16 * (0x 10 + 1)) แทนที่จะเป็น 0x 100 ดังนั้น,ถ้า x.len() มากกว่า 0x 10 โปรแกรมจะเขียนไปยัง state.offset(0x 10) ของหน่วยความจำที่ไม่ได้จัดสรร

เมื่อโทร fn_dump_state()หาก x.len() > 0x 10 จะทำให้โหนด L2 หยุดทำงานทั่งตี๋จะเปิดให้บริการเร็วๆ นี้เริ่มต้นใหม่และจากสภาพที่ถูกทิ้งไปก่อนหน้านี้กำลังโหลดสถานะ

ช่วงเวลาดัมพ์สถานะคือ 5 วินาที แต่ตราบใดที่ตัวทวนสัญญาณจับเหตุการณ์ SendRemoteMessage มันจะส่งต่อข้อความหากโหนด L2 ขัดข้องเมื่อมีการรวมธุรกรรมการถ่ายโอนข้ามสายโซ่ใหม่ในบล็อก แต่สถานะล่าสุดไม่ได้ถูกทิ้ง ข้อความจะถูกส่งต่อไปยัง L1 และสถานะของ L2 สามารถกู้คืนไปยังสถานะก่อนเท่านั้น การโอนเกิดขึ้น ในกรณีนี้ ผู้ใช้สามารถโอนเงินไปที่ L1 โดยไม่ต้องใช้เงินใดๆ ใน L2

มีเพียง SimpleMultiSigGov ที่ 0x 31337 เท่านั้นที่สามารถโต้ตอบกับ ADMIN ได้ แต่เราไม่สามารถรับลายเซ็นที่ถูกต้องเพื่อดำเนินธุรกรรมได้ นอกจากนี้เรายังสามารถใช้ชุดความครอบคลุมของรัฐเพื่อเขียนทับรหัสชั่วคราวที่ 0x 31337 และจำลองการโทร

ฟังก์ชัน admin_func_run() ของ ADMIN เป็นจุดเริ่มต้น หากต้องการเรียกใช้ฟังก์ชัน fn_dump_state() สองไบต์แรกควรเป็น 0x 0204

สารละลาย

การใช้ pwn และเครื่องมืออื่นๆ ทำให้เราสามารถดำเนินการต่างๆ เพื่อกระตุ้นให้โหนด L2 หยุดทำงาน จากนั้นจึงทำการถ่ายโอนข้ามสายโซ่เมื่อโหนด L2 รีสตาร์ทและโหลดจากสถานะก่อนหน้า ด้วยวิธีนี้เราสามารถย้ายเงินทุนไปที่ L1 โดยไม่ต้องใช้เงินทุนกับ L2 จริงๆกระบวนการนี้ต้องการการควบคุมเวลาที่แม่นยำและการทำงานของสถานะโหนด L2

11. Dragon Tyrant 

ความท้าทายนี้เป็นเกมกับมังกร เอาชนะมังกรเพื่อชนะและผ่านความท้าทาย มีช่องโหว่ที่สำคัญสองประการในความท้าทายนี้:

(1) ตัวเลขสุ่มที่คาดเดาได้:กระบวนการสร้างตัวเลขสุ่มของเกมสามารถทำนายได้ และตัวเลขสุ่มนี้จะกำหนดการตัดสินใจโจมตี/ป้องกัน ซึ่งจะส่งผลต่อผลลัพธ์ของเกม ตัวสร้างตัวเลขสุ่มของเกมอาศัยเมล็ดที่คาดเดาได้ซึ่งสามารถรับได้ล่วงหน้าโดยการตรวจสอบธุรกรรมบล็อกเชนเฉพาะ (แก้ไขการสุ่ม) โซลูชันของเราคือการตรวจสอบและรวบรวมข้อมูลเริ่มต้นให้เพียงพอผ่าน Listener พูลธุรกรรม จากนั้นจึงใช้ข้อมูลนี้เพื่อคาดการณ์ข้อมูลเริ่มต้นถัดไป

(2) ช่องโหว่ทางตรรกะ:เมื่อผู้เล่นสวมใส่ดาบและโล่ในตำนานเท่านั้นจึงจะสามารถเพิ่มมูลค่าการโจมตีและการป้องกันได้สูงสุด สัญญาเกมอนุญาตให้ผู้เล่นส่งที่อยู่ตามสัญญาร้านค้าของตนเองเพื่อซื้ออุปกรณ์ และกลไกในการตรวจสอบว่าร้านค้าแบบกำหนดเองนี้ถูกต้องตามกฎหมายนั้นขึ้นอยู่กับการเปรียบเทียบโค้ดแฮชของสัญญาร้านค้ามากกว่าที่อยู่ ซึ่งหมายความว่าหากผู้เล่นสามารถสร้างสัญญาด้วยโค้ดแฮชเดียวกันกับร้านค้าอย่างเป็นทางการแต่มีคอนสตรัคเตอร์ที่แตกต่างกัน พวกเขาสามารถข้ามขั้นตอนการซื้อปกติและขีดจำกัดราคาได้ โซลูชันของเราใช้ประโยชน์จากช่องโหว่นี้โดยการสร้างสัญญาร้านค้าแบบกำหนดเองเพื่อซื้อดาบและโล่ในตำนาน โดยไม่ต้องเสียค่าใช้จ่ายในการซื้อที่สูง

พื้นหลังของเกม

พื้นหลังความท้าทายนี้เป็นมินิเกม ในเกมมีมังกรที่มีพละกำลัง/ร่างกายขั้นสุดยอด (พลังโจมตี/พลังป้องกันสูง) และพลังชีวิต 60 แต้มและคุณในฐานะตัวเอกได้สุ่มสร้างคุณลักษณะที่อ่อนแอและ 1 คะแนนสุขภาพมังกรตัวนี้จะต้องพ่ายแพ้ ทั้งคุณและมังกรต่างก็เป็นโทเค็น ERC 721 เมื่อมังกรอยู่ในการต่อสู้ล้มเหลวและถูกทำลายในเวลาต่อมาชั่วโมง(ตรวจสอบวิธีแก้ปัญหา) ความท้าทายเท่ากับความสำเร็จ

คุณต้องต่อสู้ต่อสู้มีมินิรอบมากถึง 256 รอบ. ในแต่ละเทิร์น คุณและมังกรสามารถทำได้เลือกโจมตีหรือป้องกันการคำนวณความเสียหายสรุปได้ดังนี้:

หลังจากแต่ละรอบเล็ก คะแนนสุขภาพของทั้งสองฝ่ายจะลดลงตามความเสียหายที่สอดคล้องกัน เมื่อพลังชีวิตของปาร์ตี้ถึง 0 มันจะถูกทำลายและเกมจะจบลง หากคะแนนสุขภาพของทั้งสองฝ่ายถึง 0ฝ่ายโจมตี - ผู้เล่น - จะถูกทำลาย

คุณลักษณะการโจมตี/ป้องกันของมังกรและผู้เล่นคือคำนวณตามคุณสมบัติและอุปกรณ์ที่เกี่ยวข้อง. แต่ละฝ่ายสามารถติดตั้งอาวุธได้หนึ่งชิ้นและโล่หนึ่งอันในร้านมีอุปกรณ์บางอย่างขายรวมทั้งดาบที่ทรงพลังมากด้วย. มังกรจะไม่ติดตั้งอะไรเลยและผู้เล่นจะมีในตอนแรก1000 ETH

มีสองสถานที่ในเกมที่ใช้ตัวสร้างตัวเลขสุ่ม หนึ่งสำหรับกำหนดคุณสมบัติของผู้เล่นอีกอันสำหรับกำหนดการตัดสินใจโจมตี/ป้องกันของมังกรการใช้เครื่องกำเนิดตัวเลขสุ่มตาม ECC,เมล็ดพันธุ์ให้บริการโดยนอกเครือข่าย

การตัดสินใจโจมตี/ป้องกัน

เพื่อเอาชนะมังกร เราต้องลดพลังชีวิตของมันลงเหลือ 0 โดยยังคงรักษาพลังชีวิตเพียงตัวเดียวของเราไว้ เมื่อพิจารณาเมทริกซ์การโจมตี/การป้องกัน นั่นหมายความว่าเราไม่สามารถปล่อยให้สถานการณ์การโจมตี/การโจมตีเกิดขึ้นได้ ในระหว่างเทิร์นการโจมตี/โจมตี พลังชีวิตของผู้เล่นทั้งสองจะลดลงเหลือ 0 ส่งผลให้ผู้เล่นล้มเหลว เนื่องจากคุณลักษณะการโจมตีของทั้งสองฝ่ายสูงกว่าพลังชีวิตของอีกฝ่ายมาก เนื่องจากรอบป้องกัน-ป้องกันนั้นคล้ายคลึงกับ NOP เราจึงวางใจได้เฉพาะรอบโจมตี/ป้องกัน และรอบป้องกัน/โจมตีเท่านั้น

เนื่องจากการตัดสินใจโจมตี/ป้องกันของทั้งสองฝ่ายนั้นส่งล่วงหน้าใช่ เราต้องรู้ตัวเลือกของมังกรล่วงหน้าเพื่อหลีกเลี่ยงการโจมตี/รอบการโจมตี สิ่งนี้ทำให้เราต้องคาดการณ์การสร้างตัวเลขสุ่มแล้วเราก็ต้องทำนายเมล็ดสุ่ม. โชคดีมีห้องสมุดหลามผลลัพธ์ของโมดูลสุ่มของ Python สามารถทำนายได้เมื่อสังเกตเอาต์พุตประมาณ 20,000 บิตจากตัวสร้าง

แล้วเราจะจัดเตรียมเอาต์พุต 20 k-bit ของโมดูลสุ่ม Python ที่เราไม่สามารถเข้าถึงได้ให้กับไลบรารีนี้ได้อย่างไร ปรากฎว่าเราทำได้แคสต์ผู้เล่นจำนวนเท่าใดก็ได้ธุรกรรมการทำเหรียญแต่ละครั้งจะทริกเกอร์ผู้ให้บริการเมล็ดพันธุ์นอกเครือข่ายเพื่อส่งเมล็ดพันธุ์แบบสุ่มเราสามารถจับเมล็ดพันธุ์ได้โดยการตรวจสอบธุรกรรมเหล่านี้ในกลุ่มธุรกรรมที่รอดำเนินการ ที่จริงแล้ว เราพบว่าหลังจากจับเมล็ดมิ้นต์ได้ 78 เมล็ด เราก็สามารถทำนายเมล็ดแบบสุ่มได้:

เนื่องจากตัวสร้างตัวเลขสุ่ม ECC นั้นถูกกำหนดไว้แล้ว เราจึงสามารถทำนายการตัดสินใจโจมตี/ป้องกันของมังกรได้ เราทำตรงกันข้ามกับมังกรเสมอ ถ้ามังกรโจมตีเราก็ป้องกัน หากมังกรป้องกัน เราก็โจมตี

คุณสมบัติการโจมตี/การป้องกัน

หากไม่มีอุปกรณ์ใด ๆ คุณลักษณะที่ถ่อมตัวของเราจะทำให้เราล้มเหลวในการต่อสู้ มังกรมีคุณสมบัติการโจมตีประเภท (uint 40).max และคุณสมบัติการป้องกันประเภท (uint 40).max - 1 หากไม่มีอุปกรณ์ใดๆ เมื่อเราโจมตีและมังกรป้องกัน เราจะไม่สร้างความเสียหายให้กับมังกร เมื่อมังกรโจมตีและเราป้องกัน เราก็ล้มเหลวทันที

แน่นอนว่าเราหันความสนใจไปที่ดาบในตำนาน. ด้วยดาบนี้ คุณลักษณะการโจมตีของเราจะถึงประเภท (uint 40) สูงสุด ทำให้เราสามารถสร้างความเสียหายให้กับมังกรได้ 1 HP เมื่อเราโจมตีและมังกรป้องกัน ถ้าเราทำซ้ำขั้นตอนนี้ 60 ครั้ง มังกรก็จะตาย มีความหวัง.

เราจะซื้อดาบเล่มนี้ได้อย่างไร ราคา 1 ล้าน ETH และเรามีเงินเพียง 1,000 ETH เท่านั้น ปรากฎว่าเมื่อเราสวมดาบเล่มนี้เกมดังกล่าวช่วยให้เราสามารถผ่านสัญญาในร้านค้าได้ด้วยตัวเองและตราบเท่าที่สัญญาร้านค้าได้รับการอนุมัติจากเจ้าของสัญญาโรงงานแล้วเกมจะดำเนินต่อไปอย่างมีความสุข เมื่อตรวจสอบอย่างใกล้ชิดปรากฎว่าการตรวจสอบนี้ไม่ได้กระทำโดยการตรวจสอบที่อยู่สัญญาของร้านค้า แต่โดยเปรียบเทียบโค้ดแฮชของสัญญาร้านค้าทำให้สมบูรณ์. ซึ่งหมายความว่าตราบใดที่เราผ่านสัญญาร้านค้าด้วยโค้ดแฮชเดียวกัน เราก็สามารถดำเนินการต่อได้ เนื่องจาก extcodehash ไม่มีตัวสร้างเราสามารถสร้างร้านขายไอเท็มของเราเองด้วยรหัสเดียวกันแต่มีคอนสตรัคเตอร์ที่แตกต่างกัน และใช้มันเพื่อจัดเตรียมดาบให้ผู้เล่นของเรา

วิธีนี้ได้ผล การใช้ร้านค้าปลอมกับคอนสตรัคเตอร์ต่อไปนี้ เราจะได้รับดาบในตำนานและโล่ในตำนานใหม่:

ด้วยอุปกรณ์ในตำนานสองชิ้นนี้ เราจะใช้คุณสมบัติการโจมตีประเภท (uint 40).max และคุณสมบัติการป้องกันประเภท (uint 40).max เมื่อมังกรโจมตี เราจะไม่สูญเสีย HP ใด ๆ และเมื่อเราโจมตี เราทำความเสียหาย 1 HP ให้กับมังกร

สารละลาย

นี่คือกระบวนการทีละขั้นตอนสำหรับการแก้ปัญหา:

  1. ผู้เล่น Mint โทเค็นเข้ากระเป๋าเงินของเราเอง

  2. ปรับใช้ร้านค้าปลอมและใช้มันเพื่อจัดเตรียมอุปกรณ์ระดับตำนานสองชิ้นให้กับผู้เล่น

  3. ปรับใช้สัญญาผู้โจมตีตามที่กำหนดโดยการท้าทาย สัญญานี้จะเข้าควบคุมโทเค็นของผู้เล่น เริ่มการต่อสู้ และให้ผู้เล่นตัดสินใจโจมตี/ป้องกัน

  4. โอนโทเค็นของผู้เล่นไปยังสัญญาของผู้โจมตี

  5. เริ่มต้นการฟังพูลธุรกรรมที่ค้างอยู่เพื่อตรวจสอบธุรกรรมการแก้ไขแบบสุ่ม โดยจะจับเมล็ดพันธุ์และทำนายเมล็ดพันธุ์ต่อไปหลังจากรวบรวมข้อมูลได้เพียงพอ

  6. สร้างโทเค็นผู้เล่นเพิ่มเติม 78 อัน

  7. ณ จุดนี้ ผู้ฟังพูลควรรวบรวมข้อมูลเพียงพอที่จะทำนายเมล็ดพันธุ์ถัดไป

  8. เมล็ดที่ทำนายไว้จะถูกป้อนเข้าไปในเครื่องสร้างตัวเลขสุ่มเพื่อพิจารณาการตัดสินใจโจมตี/ป้องกันของมังกร

  9. ย้อนกลับสตริงการตัดสินใจของมังกรในระดับบิตเพื่อให้ได้การตัดสินใจของผู้เล่น เมื่อสอบถาม สัญญาฝ่ายรุกจะให้การตัดสินใจของผู้เล่น

  10. สัญญาผู้โจมตีเปิดการโจมตี ส่งผลให้มังกรล้มเหลว

12. Hopping Into Place

เป้าหมายของความท้าทายนี้คือการถอนเงินทั้งหมดออกจากสัญญาสะพานข้ามสายโซ่ ช่องโหว่นี้มีอยู่ในฟังก์ชัน _เพิ่มเติมDebit() เมื่อฟังก์ชันนี้คำนวณความรับผิดของ Bonder หากกำหนด ChallengePeriod เป็น 0 จะไม่มีการเพิ่มหนี้ วิธีแก้ปัญหาของเราคือการใช้ประโยชน์จากช่องโหว่นี้โดยการตั้งค่า ChallengePeriod เป็น 0 เพื่อให้ numTimeSlots เป็น 0 ด้วย ดังนั้นจึงป้องกันไม่ให้หนี้เพิ่มขึ้น ต่อไป เราใช้ฟังก์ชัน BondTransferRoot() เพื่อถอนโทเค็นจำนวนเท่าใดก็ได้ เนื่องจากฟังก์ชัน getDebitAndAdditionalDebit() จะสูญเสียการทำงานเดิมในกรณีนี้ ส่งผลให้หนี้ไม่เพิ่มขึ้น ด้วยวิธีนี้ เราระบายเงินทุนในสะพานข้ามสายโซ่ได้สำเร็จ

การวิเคราะห์ช่องโหว่

ในความท้าทายนี้ ตัวตนของเราคือผู้ควบคุม ดังนั้นเราจึงสามารถเปลี่ยนการกำหนดค่าบางอย่างของสะพานข้ามสายโซ่ได้

เราสังเกตเห็นว่าต้นตอของปัญหาอยู่ที่ฟังก์ชัน _เพิ่มเติมDebit()หนี้จะถูกเพิ่มเข้าไปในคำสั่ง if ซึ่งหมายความว่าหาก numTimeSlots เท่ากับ 0 คำสั่งจะไม่ถูกดำเนินการความรับผิดของผู้ค้ำประกันไม่เพิ่มขึ้น แน่นอนว่าการออกแบบนี้ไม่สมเหตุสมผล หนี้ที่เพิ่มขึ้นไม่ควรข้ามไปไม่ว่าในกรณีใด ๆ

เราสามารถใช้ประโยชน์จากสิ่งนี้และบรรลุเงื่อนไขที่ numTimeSlots เป็น 0 โดยการตั้งค่า ChallengePeriod เป็น 0

ด้วยวิธีนี้ ฟังก์ชัน getDebitAndAdditionalDebit จะสูญเสียฟังก์ชันการทำงานเพิ่มเติมทำยังไงหนี้ก็ไม่เพิ่ม

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

สุดท้ายนี้ เรามาดูตรรกะใน BondTransferRoot กัน ฟังก์ชันนี้จะตั้งค่า TotalAmount ให้กับหนี้ของผู้โทร และเพิ่ม TotalAmount ให้กับ TransferRoots เพื่อแยกข้อมูล ดังนั้นเราจึงสามารถใช้ฟังก์ชันนี้เพื่อถอนโทเค็นจำนวนเท่าใดก็ได้

สารละลาย

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

  1. สัญญาการหาประโยชน์: นี่คือสัญญาหลักของการโจมตีและมีหน้าที่รับผิดชอบในการดำเนินการตามกระบวนการโจมตีทั้งหมด เป็นครั้งแรกที่เกี่ยวข้องกับสัญญาท้าทายท้าทาย จากนั้นจัดการสะพานข้ามสายโซ่ IBridge ผ่านชุดการดำเนินงาน และในที่สุดก็บรรลุวัตถุประสงค์ของการถอนเงิน

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

  3. แก้ไขสัญญา: สัญญานี้สืบทอดมาจาก CTFSolver และใช้เพื่อโต้ตอบกับความท้าทายด้านสัญญาความท้าทายในการท้าทาย Capture The Flag (CTF) มีหน้าที่หลักในการเรียกวิธีการใช้ประโยชน์จากสัญญา Exploit เพื่อดำเนินการโจมตี และยืนยันว่าความท้าทายได้รับการแก้ไขหรือไม่หลังจากการโจมตีสำเร็จแล้ว

  4. อินเทอร์เฟซ IBridge: นี่คืออินเทอร์เฟซที่กำหนดวิธีการทำสัญญาสะพานข้ามสายโซ่ รวมถึงวิธีการดำเนินงานสะพานข้ามสายโซ่ที่ใช้ในสัญญา Exploit เช่น การเพิ่มผู้ค้ำประกัน การกำหนดเวลาท้าทาย การผูกมัดการโอนรูท การถอนเงิน ฯลฯ

  5. อินเทอร์เฟส IChallenge: อินเทอร์เฟสนี้กำหนดวิธีการใน Challenge Contract Challenge โดยอนุญาตให้สัญญา Exploit เข้าถึงที่อยู่ของสะพานข้ามสายโซ่ในการท้าทาย

13. Oven

เป้าหมายของความท้าทายนี้คือการกู้คืนค่า FLAG ที่ซ่อนอยู่ หัวใจสำคัญของความท้าทายคือฟังก์ชัน fiat_shamir() ซึ่งใช้ฟังก์ชันแฮชแบบกำหนดเอง custom_hash() เพื่อสร้างตัวเลขสุ่ม จากนั้นใช้ตัวเลขนี้เพื่อเข้าร่วมในการคำนวณ ช่องโหว่ที่สำคัญอยู่ในฟังก์ชัน fiat_shamir() โดยเฉพาะในนิพจน์ r=(v - c * FLAG) mod (p-1) ซึ่งเกี่ยวข้องกับค่า r, c, p ที่รู้จักและค่า FLAG ที่ไม่รู้จัก วิธีแก้ไขคือเปลี่ยนปัญหาให้เป็นปัญหาขัดแตะ จากนั้นใช้อัลกอริธึมการลดขนาดพื้นฐานขัดแตะ (อัลกอริทึม LLL) เพื่อค้นหาค่า FLAG

การวิเคราะห์ช่องโหว่

ฟังก์ชันโค้ด: ผู้ใช้สามารถรับลายเซ็นแบบสุ่มของ FLAG และตรรกะสำหรับการสร้างลายเซ็นแบบสุ่มจะอยู่ในฟังก์ชัน fiat_shamir() ฟังก์ชันแฮชแบบกำหนดเอง custom_hash ใช้เพื่อสร้างค่าแฮช ซึ่งเรียกอัลกอริธึมการแฮชที่แตกต่างกันสี่แบบ ดังนั้นจึงไม่สามารถถอดรหัสการสุ่มได้ในปัจจุบัน

นอกจากนี้ การแปลง fiat_shamir ยังเป็นเครื่องมือที่สำคัญมากในการเข้ารหัส โดยหลักคือ การใช้อัลกอริธึมแฮชเพื่อสร้างตัวเลขสุ่มและเพิ่มการสุ่มให้กับโปรโตคอลการเข้ารหัส การใช้งานทั่วไปของการเปลี่ยนแปลง FS คือการแนะนำการไม่โต้ตอบเข้าสู่ระบบพิสูจน์ความรู้เป็นศูนย์ จากนั้นจึงสร้างโปรโตคอล เช่น snark และ stark

จากซอร์สโค้ด เราสามารถรับ t, r, p, g, y และข้อมูลอื่นๆ ได้ แต่จริงๆ แล้ว c สามารถคำนวณได้โดยใช้ฟังก์ชัน custom_hash() ดังนั้นช่องโหว่จึงรวมอยู่ในฟังก์ชัน fiat_shamir() ซึ่งเป็นส่วนการทำงานที่ส่งสัญญาณ FLAG โดยเน้นที่: r=(v - c * FLAG) mod (p-1) สำหรับสมการนี้ ข้อมูลที่เราหาได้ในปัจจุบันคือ r, c และ p เป็นค่าที่ทราบทั้งหมด และจำนวนบิตของ FLAG ถูกกำหนดไว้แล้ว: assert FLAG.bit_length()<384 。 อาจเกี่ยวข้องกับปัญหา HNP (ที่มีโมดูลัสแปรผัน) ที่เสนอโดย Dan Boneh ในปี 1996 และสามารถโจมตีได้โดยใช้อัลกอริธึมขัดแตะมาตรฐานสำหรับรายละเอียดเพิ่มเติมของการวิเคราะห์การเข้ารหัสของการโจมตีแบบ Lattice โปรดดูที่เอกสารที่เกี่ยวข้อง

ปัญหาคือ r=(v - c * FLAG) mod (p-1) ในโค้ด เนื่องจาก r, c และ p เป็นค่าที่ทราบทั้งหมด ดังนั้น:

  1. ขั้นแรก แปลงสมการข้างต้นทางคณิตศาสตร์: r-v+c*FLAG= 0 mod (p-1) โดยที่ไม่ทราบเฉพาะ v และ FLAG

  2. ประการที่สอง สร้างตาข่าย:โดยที่ K คือขอบเขตบนของ FLAG และช่องว่างทั้งหมดคือ 0

  3. ตามอัลกอริทึมโซลูชัน CVP ของ Babai จะต้องมีเวกเตอร์โซลูชัน j=[l1, l2, l3, FLAG, 1] ทำให้ jM=jk เป็นจริง

  4. โปรดทราบว่า jk เป็นเวกเตอร์ขนาดสั้นในโครงตาข่าย ดังนั้นเราสามารถค้นหาเวกเตอร์ขนาดสั้นนี้ในเวลาพหุนามได้โดยใช้อัลกอริทึม LLL โปรดทราบว่าแต่ละองค์ประกอบของเวกเตอร์ขนาดสั้นสามารถแสดงได้ด้วย 64 บิต ดังนั้นขอบเขตบน K= 2^64 จึงถูกกำหนด

เคล็ดลับ: ต่อไปนี้เป็นหมายเหตุเกี่ยวกับปัญหาปริมาณข้อมูล เราจะรู้ได้อย่างไรว่าต้องใช้ข้อมูลจำนวนเท่าใดในการกู้คืน FLAG ซึ่งต้องใช้การวิเคราะห์พฤติกรรมแบบเกาส์เซียนเพื่อประมาณความยาวเวกเตอร์ที่สั้นที่สุด และบรรทัดฐานเวกเตอร์เป้าหมายที่ต้องการนั้นเล็กกว่าความยาวนี้ อย่างไรก็ตาม เนื่องจากนี่คือบริบทของการแข่งขัน CTF โดยปกติแล้วจึงสามารถใช้ข้อมูลสามถึงสี่หรือห้าชุดในขั้นต้นได้ ถ้าไม่เช่นนั้น คุณสามารถใช้วิธีการข้างต้นเพื่อการคำนวณที่แม่นยำได้ ที่นี่,เรารวบรวมข้อมูลห้าชุดเพื่อการสำรองข้อมูล แต่จริงๆ แล้วมีเพียงสามชุดเท่านั้นที่ใช้ข้อมูลเพื่อแก้ปัญหา FLAG

สารละลาย

โค้ดของเราจำเป็นต้องทำงานในสภาพแวดล้อมแบบ sage-python แนวคิดหลักมีดังนี้:

  1. สร้างขัดแตะ: ขั้นแรกให้สร้างขัดแตะเฉพาะซึ่งมีค่า p, c, r ที่รู้จักและค่า FLAG ที่ไม่รู้จัก ตารางนี้ถูกแปลงจากสมการข้างต้น

  2. การใช้อัลกอริทึม LLL: ใช้อัลกอริทึม LLL เพื่อค้นหาเวกเตอร์ขนาดสั้นในโครงตาข่าย อัลกอริธึม LLL เป็นอัลกอริธึมที่มีประสิทธิภาพซึ่งสามารถค้นหาเวกเตอร์พื้นฐานของแลตทิซในเวลาพหุนามซึ่งสัมพันธ์กันทางคณิตศาสตร์กับการแก้ปัญหาดั้งเดิม

  3. การกู้คืน FLAG: เมื่อพบเวกเตอร์แบบสั้นแล้ว ค่าของ FLAG ก็สามารถดึงออกมาได้ เนื่องจากองค์ประกอบในเวกเตอร์แบบสั้นสามารถแสดงเป็น 64 บิตได้ จึงเป็นการกำหนดขีดจำกัดบนของขนาดของ FLAG

จากการแข่งขันสู่การฝึกฝน

ทีมงาน Salute ได้รับประสบการณ์อันทรงคุณค่าในการแข่งขัน Paradigm CTF 2023 ซึ่งขณะนี้บริการตรวจสอบสัญญาอัจฉริยะที่ได้รับการปรับปรุงโดย Salus Securityส่วนสำคัญของ. หากคุณต้องการบริการตรวจสอบสัญญาอัจฉริยะชั้นยอด อย่าลังเลที่จะทำติดต่อเรา. เรามุ่งมั่นที่จะให้การสนับสนุนที่ครอบคลุมและมีประสิทธิภาพสำหรับความต้องการของคุณ

ความปลอดภัย
สัญญาที่ชาญฉลาด
Paradigm
ยินดีต้อนรับเข้าร่วมชุมชนทางการของ Odaily
กลุ่มสมาชิก
https://t.me/Odaily_News
กลุ่มสนทนา
https://t.me/Odaily_CryptoPunk
บัญชีทางการ
https://twitter.com/OdailyChina
กลุ่มสนทนา
https://t.me/Odaily_CryptoPunk
สรุปโดย AI
กลับไปด้านบน
Salus安全团队在Paradigm 2023 CTF中共解决了13项挑战,在1011支队伍中以3645.60的分数获得第九名,并受邀成为Paradigm CTF 2024的客座作者。在这篇博文中,我们将介绍我们在比赛期间解决的所有挑战。
คลังบทความของผู้เขียน
Salus Insights
อันดับบทความร้อน
Daily
Weekly
ดาวน์โหลดแอพ Odaily พลาเน็ตเดลี่
ให้คนบางกลุ่มเข้าใจ Web3.0 ก่อน
IOS
Android