คำเตือนความเสี่ยง: ระวังความเสี่ยงจากการระดมทุนที่ผิดกฎหมายในนาม 'สกุลเงินเสมือน' 'บล็อกเชน' — จากห้าหน่วยงานรวมถึงคณะกรรมการกำกับดูแลการธนาคารและการประกันภัย
ข่าวสาร
ค้นพบ
ค้นหา
เข้าสู่ระบบ
简中
繁中
English
日本語
한국어
ภาษาไทย
Tiếng Việt
BTC
ETH
HTX
SOL
BNB
ดูตลาด
เจาะลึกว่าการโจมตีกลับคืนชีพขโมยเงิน 70 ล้านดอลลาร์จากแหล่งรวมของ Curve ได้อย่างไร
jk
Odaily资深作者
2023-08-07 00:58
บทความนี้มีประมาณ 2222 คำ การอ่านทั้งหมดใช้เวลาประมาณ 4 นาที
วิทยาศาสตร์ทางเทคนิคเกี่ยวกับการโจมตีกลับคืนสู่สภาพเดิม

ต้นฉบับA Deep Dive Into How Curve Pool’s $ 70 Million Reentrancy Exploit Was Possibleต้นฉบับ

ผู้แต่ง: แอน เรียบเรียงโดย Odaily jk

การละเมิด Curve Pool ล่าสุดนั้นแตกต่างจากการแฮ็กสกุลเงินดิจิทัลส่วนใหญ่ที่เราเคยเห็นในช่วงไม่กี่ปีที่ผ่านมา เนื่องจากไม่เหมือนกับการละเมิดครั้งก่อน ๆ การแฮ็กนี้ไม่เกี่ยวข้องโดยตรงกับช่องโหว่ในสัญญาอัจฉริยะ แต่ขึ้นอยู่กับ บนคอมไพเลอร์พื้นฐานของภาษาที่ใช้

ที่นี่เรากำลังพูดถึง Vyper: ภาษาการเขียนโปรแกรม Pythonic สำหรับสัญญาอัจฉริยะที่ออกแบบมาเพื่อเชื่อมต่อกับ Ethereum Virtual Machine (EVM) ฉันสนใจมากเกี่ยวกับสิ่งที่อยู่เบื้องหลังช่องโหว่นี้ ดังนั้นฉันจึงตัดสินใจเจาะลึกลงไป

ช่องโหว่นี้เรียกว่า Reentrancy Bug และมีอยู่ในภาษาการเขียนโปรแกรม Vyper บางเวอร์ชัน โดยเฉพาะ v 0.2.15, v 0.2.16 และ v 0.3.0 ดังนั้นโครงการทั้งหมดที่ใช้ Vyper เวอร์ชันเฉพาะเหล่านี้จึงสามารถกำหนดเป้าหมายได้

ชื่อระดับแรก

การกลับเข้ามาใหม่คืออะไร?

เพื่อทำความเข้าใจว่าเหตุใดช่องโหว่นี้จึงเกิดขึ้น เราต้องเข้าใจก่อนว่าการกลับเข้ามาใหม่คืออะไรและทำงานอย่างไร

กล่าวกันว่าฟังก์ชันจะกลับเข้าใหม่หากสามารถถูกขัดจังหวะระหว่างการดำเนินการและสามารถเรียกได้อีกครั้งอย่างปลอดภัย (เข้าใหม่) ก่อนที่การโทรครั้งก่อนจะเสร็จสิ้นการดำเนินการ ฟังก์ชัน Reentrant ใช้ในแอปพลิเคชัน เช่น การจัดการการขัดจังหวะด้วยฮาร์ดแวร์และการเรียกซ้ำ

  • เพื่อให้ฟังก์ชันกลับมาใหม่ได้ จะต้องเป็นไปตามเงื่อนไขต่อไปนี้:

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

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

ไม่ควรเรียกใช้ฟังก์ชันอื่นๆ ที่ไม่ใช่การกลับเข้าใหม่ ไม่ควรสับสนการกลับเข้าใหม่กับความปลอดภัยของเธรด แม้ว่าจะเกี่ยวข้องกันอย่างใกล้ชิดก็ตาม ฟังก์ชันสามารถเป็นเธรดที่ปลอดภัยและยังคงไม่สามารถกลับเข้าใหม่ได้ เพื่อหลีกเลี่ยงความสับสน การกลับเข้ามาใหม่เกี่ยวข้องกับเธรดการดำเนินการเพียงเธรดเดียวเท่านั้น นี่เป็นแนวคิดในยุคที่ไม่มีระบบปฏิบัติการแบบมัลติทาสกิ้ง

i = 5 
def non_reentrant_function():
  return i** 5 
def reentrant_function(number:int):
  return number** 5 

นี่เป็นตัวอย่างที่เป็นประโยชน์:

  • ฟังก์ชัน non_reentrant_function:

  • ฟังก์ชันนี้ไม่มีพารามิเตอร์

  • มันจะส่งคืนตัวแปรโกลบอลที่ฉันยกกำลังที่ห้าโดยตรง

ดังนั้นเมื่อคุณเรียกใช้ฟังก์ชันนี้ มันจะคืนค่า 5** 5 ซึ่งก็คือ 3125 เสมอ

  • ฟังก์ชัน reentrant_function:

  • ฟังก์ชันนี้มีหมายเลขพารามิเตอร์ซึ่งเป็นจำนวนเต็ม

  • ส่งกลับหมายเลขอาร์กิวเมนต์ที่ยกกำลังห้า

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

ชื่อระดับแรก

ล็อคคืออะไร?

การล็อคเป็นกลไกการซิงโครไนซ์เธรดโดยพื้นฐานแล้วกระบวนการหนึ่งสามารถอ้างสิทธิ์หรือ ล็อก กระบวนการอื่นได้

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

@nonreentrant('lock')
def func():
  assert not self.locked, "locked"
  self.locked = True
  # Do stuff
  # Release the lock after finishing doing stuff
  raw_call(msg.sender, b"", value= 0)
  self.locked = False
  # More code here

ภาษาการเขียนโปรแกรมใช้การล็อคเบื้องหลังเพื่อจัดการและแบ่งปันการเปลี่ยนแปลงสถานะระหว่างรูทีนย่อยหลายรายการอย่างสวยงาม อย่างไรก็ตาม บางภาษา เช่น C# และ Vyper อนุญาตให้ใช้การล็อคในโค้ดได้โดยตรง

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

ดังนั้นใน Vyper มัณฑนากร nonreentrant('lock') จึงเป็นกลไกในการควบคุมการเข้าถึงฟังก์ชันต่างๆ เพื่อป้องกันไม่ให้ผู้โทรเรียกใช้ฟังก์ชันสัญญาอัจฉริยะซ้ำๆ ก่อนที่จะทำงานเสร็จ

ในการแฮ็ก DeFi หลายครั้ง มักเป็นข้อผิดพลาดของสัญญาอัจฉริยะที่ผู้พัฒนาสัญญาไม่คาดคิด และผู้แสวงหาผลประโยชน์ที่ฉลาดแต่ประสงค์ร้ายก็ค้นพบจุดอ่อนในฟังก์ชันบางอย่างหรือวิธีที่ข้อมูลถูกเปิดเผย แต่สิ่งพิเศษเกี่ยวกับกรณีนี้คือ Smart Contract ของ Curve รวมถึงพูลและโปรเจ็กต์อื่นๆ ทั้งหมดที่ตกเป็นเหยื่อของการโจมตีนั้น ไม่มีช่องโหว่ในโค้ดเลย สัญญามีความมั่นคง

nonreentrant('lock') มีอยู่

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

@nonreentrant('lock')
def remove_liquidity(
    _burn_amount: uint 256,
    _min_amounts: uint 256 [N_COINS],
    _receiver: address = msg.sender
) -> uint 256 [N_COINS]:
    """
    @notice Withdraw coins from the pool
    @dev Withdrawal amounts are based on current deposit ratios
    @param _burn_amount Quantity of LP tokens to burn in the withdrawal
    @param _min_amounts Minimum amounts of underlying coins to receive
    @param _receiver Address that receives the withdrawn coins
    @return List of amounts of coins that were withdrawn
    """
    total_supply: uint 256 = self.totalSupply
    amounts: uint 256 [N_COINS] = empty(uint 256 [N_COINS])
    for i in range(N_COINS):
        old_balance: uint 256 = self.balances[i]
        value: uint 256 = old_balance * _burn_amount / total_supply
        assert value >= _min_amounts[i], "Withdrawal resulted in fewer coins than expected"
        self.balances[i] = old_balance - value
        amounts[i] = value
        if i == 0:
            raw_call(_receiver, b"", value=value)
        else:
            response: Bytes[ 32 ] = raw_call(
                self.coins[ 1 ],
                concat(
                    method_id("transfer(address, uint 256)"),
                    convert(_receiver, bytes 32),
                    convert(value, bytes 32),
                ),
                max_outsize= 32,
            )
            if len(response) > 0:
                assert convert(response, bool)
    total_supply -= _burn_amount
    self.balanceOf[msg.sender] -= _burn_amount
    self.totalSupply = total_supply
    log Transfer(msg.sender, ZERO_ADDRESS, _burn_amount)
    log RemoveLiquidity(msg.sender, amounts, empty(uint 256 [N_COINS]), total_supply)
    return amounts

ชื่อระดับแรก

สิ่งนี้ถูกเอารัดเอาเปรียบอย่างไร?

จนถึงตอนนี้ เรารู้ว่าการโจมตีซ้ำเป็นวิธีการเรียกฟังก์ชันบางอย่างซ้ำๆ ในสัญญาอัจฉริยะ แต่สิ่งนี้นำไปสู่การขโมยเงินทุนและการสูญเสียเงิน 70 ล้านดอลลาร์ในการโจมตี Curve ได้อย่างไร

สังเกตเห็น self.balanceOf[msg.sender] -= _burn_amount ที่ส่วนท้ายของสัญญาอัจฉริยะหรือไม่ สิ่งนี้จะบอกสัญญาอัจฉริยะถึงสภาพคล่องของ msg.sender ในพูล ลบด้วยค่าธรรมเนียมการเผาไหม้ บรรทัดต่อไปนี้ของการโทรด้วยรหัส transfer() บน message.sender

ดังนั้นสัญญาที่เป็นอันตรายสามารถเรียกการถอนเงินได้อย่างต่อเนื่องก่อนที่จะอัปเดตจำนวนเงิน ทำให้พวกเขามีทางเลือกในการถอนสภาพคล่องเกือบทั้งหมดในกลุ่ม

  • การโจมตีตามปกติจะเป็นดังนี้:

  • สัญญาที่มีช่องโหว่มี 10 eth

  • ผู้โจมตีเรียกการฝากและฝาก 1 eth

  • ผู้โจมตีเรียกร้องให้ถอน 1 eth และฟังก์ชันการถอนจะทำการตรวจสอบบางอย่างในเวลานี้:

  • ผู้โจมตีมี 1 eth ในบัญชีของพวกเขาหรือไม่? ใช่.

  • โอน 1 eth ไปยังสัญญาที่เป็นอันตราย หมายเหตุ: ยอดคงเหลือของสัญญาไม่เปลี่ยนแปลง เนื่องจากฟังก์ชันยังคงดำเนินการอยู่

  • ผู้โจมตีโทรอีกครั้งเพื่อถอน 1 eth (เข้าใหม่)

ผู้โจมตีมี 1 eth ในบัญชีของพวกเขาหรือไม่? ใช่.

นี้จะทำซ้ำจนกว่าจะไม่มีสภาพคล่องในสระอีกต่อไป

Curve
ยินดีต้อนรับเข้าร่วมชุมชนทางการของ Odaily
กลุ่มสมาชิก
https://t.me/Odaily_News
กลุ่มสนทนา
https://t.me/Odaily_CryptoPunk
บัญชีทางการ
https://twitter.com/OdailyChina
กลุ่มสนทนา
https://t.me/Odaily_CryptoPunk
สรุปโดย AI
กลับไปด้านบน
วิทยาศาสตร์ทางเทคนิคเกี่ยวกับการโจมตีกลับคืนสู่สภาพเดิม
ดาวน์โหลดแอพ Odaily พลาเน็ตเดลี่
ให้คนบางกลุ่มเข้าใจ Web3.0 ก่อน
IOS
Android