Tin nóng ⇢

Tìm hiểu về cách một cuộc tấn công vào lại đã đánh cắp 70 triệu đô la từ quỹ của Curve

Vụ vi phạm nhóm Curve gần đây khác với hầu hết các vụ hack tiền điện tử mà chúng tôi đã thấy trong vài năm qua bởi vì, không giống như nhiều vụ vi phạm trước đây, vụ vi phạm này không liên quan trực tiếp đến lỗ hổng trong chính hợp đồng thông minh, mà liên quan trực tiếp đến lỗ hổng trong hợp đồng thông minh. trên trình biên dịch cơ bản của ngôn ngữ đang được sử dụng.

Ở đây chúng ta đang nói về Vyper: một ngôn ngữ lập trình Pythonic dành cho các hợp đồng thông minh được thiết kế để giao tiếp với Máy ảo Ethereum (EVM). Tôi rất quan tâm đến những gì đằng sau lỗ hổng này, vì vậy tôi quyết định tìm hiểu sâu hơn.

Các tiêu đề hàng ngày đang báo cáo những con số mới khi vi phạm này phát triển. Bây giờ có vẻ như tình hình cuối cùng đã được kiểm soát, nhưng không phải trước khi hơn 70 triệu đô la bị đánh cắp. Theo đánh giá sau thực tế của LlamaRisk, tính đến hôm nay, pool của một số dự án DeFi cũng đã bị hack, bao gồm pETH/ETH của PEGD: 11 triệu đô la; msETH/ETH của Metronome: 3,4 triệu đô la; Alchemix của alETH /  ETH :  2260 triệu USD và Curve DAO: khoảng 24,7 triệu USD.

Lỗ hổng này được gọi là lỗi reentrancy và nó đã xuất hiện trên một số phiên bản của ngôn ngữ lập trình Vyper, cụ thể là v 0.2.15, v 0.2.16 và v 0.3.0. Do đó, tất cả các dự án sử dụng các phiên bản Vyper cụ thể này đều có thể được nhắm mục tiêu.

  • Nó không thể sử dụng dữ liệu toàn cầu và tĩnh. Đây chỉ là một quy ước, không có giới hạn cứng, nhưng nó có thể mất thông tin nếu một chức năng sử dụng dữ liệu chung bị gián đoạn và khởi động lại.
  • Nó không nên sửa đổi mã riêng của mình. Bất cứ khi nào một chức năng bị gián đoạn, nó sẽ có thể thực thi theo cùng một cách. Điều này có thể được quản lý, nhưng thường không được khuyến khích.
  • Nó không nên gọi các chức năng không gửi lại khác. Không nên nhầm lẫn giữa sự vào lại với sự an toàn của luồng, mặc dù chúng có liên quan chặt chẽ với nhau. Một chức năng có thể là luồng an toàn và vẫn không được cấp lại. Để tránh nhầm lẫn, việc vào lại chỉ liên quan đến một luồng thực thi. Đây là một khái niệm trong thời kỳ không có hệ điều hành đa nhiệm nào tồn tại.

Đây là một ví dụ thực tế:

i = 5 
def non_reentrant_function():
  trả về i** 5 
def reentrant_function(number:int):
  trả về số** 5 

Hàm non_reentrant_function:

  • Chức năng này không có tham số.
  • Nó trực tiếp trả về biến toàn cục i đã nâng lên lũy thừa thứ năm.
  • Vì vậy, khi bạn gọi hàm này, nó luôn trả về 5** 5, tức là 3125.

Hàm reentrant_function:

  • Hàm này có một số tham số, là một số nguyên.
  • Nó trả về số đối số được nâng lên lũy thừa thứ năm.
  • Điều này có nghĩa là bạn có thể chuyển bất kỳ số nguyên nào cho hàm này và nhận số đó được nâng lên lũy thừa thứ năm làm giá trị trả về. Ví dụ: nếu bạn chuyển vào 2 , nó sẽ trả về 2 mũ 5, tức là 32.

Điều đáng chú ý là nhiều chức năng hợp đồng thông minh không được cấp lại, vì chúng truy cập thông tin toàn cầu như số dư ví.

def func():
  khẳng định không phải self.locked, “locked”
  self.locked = True
  # Thực hiện nội dung
  # Thả khóa sau khi thực hiện xong nội dung
  raw_call(msg. sender, b””, value= 0)
  self.locked = False
  # Thêm mã ở đây

Trong ví dụ trên, chúng tôi muốn đảm bảo rằng nếu msg.sender (người gọi hợp đồng) là một hợp đồng khác, thì nó sẽ không gọi mã khi thực thi. Nếu có nhiều mã bên dưới raw_call() không có khóa, msg.sender có thể gọi tất cả mã ở trên trước khi chức năng của chúng tôi thực thi xong.

Do đó, trong Vyper, trình trang trí nonreentrant(‘lock’) là một cơ chế kiểm soát quyền truy cập vào các chức năng để ngăn người gọi thực hiện liên tục các chức năng hợp đồng thông minh trước khi chúng chạy xong.

Trong nhiều vụ hack DeFi, thường là lỗi hợp đồng thông minh mà nhà phát triển hợp đồng không lường trước được và một kẻ khai thác thông minh nhưng ác ý đã phát hiện ra điểm yếu trong một số chức năng hoặc cách dữ liệu bị lộ. Nhưng điểm độc đáo của trường hợp này là các hợp đồng thông minh của Curve, cũng như tất cả các nhóm và dự án khác trở thành nạn nhân của cuộc tấn công, không có lỗ hổng nào được biết đến trong chính mã. Hợp đồng là vững chắc.

nonreentrant(‘lock’) tồn tại.

Sự cố này xảy ra do sự cố với cách ngôn ngữ Vyper xử lý khóa truy cập lại. Do đó, người tạo hợp đồng có thể triển khai mã có vẻ hợp lý, nhưng do trình biên dịch không xử lý khóa chính xác nên kẻ tấn công có thể khai thác khóa thiếu sót này, gây ra kết quả không mong muốn trong hành vi của hợp đồng.

Hãy xem xét một hợp đồng thực sự dễ bị tấn công vào lại. Lưu ý công cụ sửa đổi @nonreentrant(‘lock’)? Thông thường, điều này sẽ ngăn chặn việc tái nhập học, nhưng thực tế thì không. Kẻ tấn công có thể liên tục gọi hàm remove_liquidity() trước khi hàm trả về kết quả.

@nonreentrant(‘lock’)
def remove_liquidity(
    _burn_amount: uint 256,
    _min_amounts: uint 256 [N_COINS],
    _receiver: address = msg. sender
) -> uint 256 [N_COINS]:
    “””

@notice Rút tiền từ pool
    @ dev Số tiền rút dựa trên tỷ lệ tiền gửi hiện tại
    @param _burn_amount Số lượng mã thông báo LP để đốt khi rút tiền
    @param _min_amounts Số lượng tiền cơ bản tối thiểu để nhận
    @param _receiver Địa chỉ nhận tiền rút
    @ret urn Danh sách số tiền đã được rút đã rút
    “””

total_supply: uint 256 = self.totalSupply
    số tiền:uint 256[N_COINS] = rỗng(uint 256[N_COINS])
    cho tôi trong phạm vi(N_COINS):
        old_balance: uint 256 = self.balances[i]
        value: uint 256 = old_balance * _burn_amount / total_supply
        khẳng định giá trị >= _min_amounts[i], “Rút tiền dẫn đến ít xu hơn dự kiến”
        self.b số dư [i] = old_balance – giá trị
        số lượng[i] = giá trị
        nếu i == 0:
            raw_call(_receiver, b””, value=value)
        other:
            response: Bytes[ 32 ] = raw_call(
                self.coins[ 1 ],
                concat (
                    method_id(“transfer(address, uint 256)”),
                    convert(_receiver, byte 32),
                    convert(value, byte 32),
                ),
                max_outsize= 32,
            )
            if len(response) > 0:
                khẳng định 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. người gửi, số tiền, trống(uint 256 [N_COINS]), total_supply)
    số tiền trả lại

Điều này được khai thác như thế nào?

Cho đến nay, chúng ta biết rằng một cuộc tấn công vào lại là một phương pháp liên tục gọi một chức năng nhất định trong hợp đồng thông minh. Nhưng làm thế nào điều này dẫn đến việc đánh cắp tiền và mất 70 triệu đô la trong cuộc tấn công Curve?

Lưu ý self.balanceOf[msg.sender] -= _burn_amount ở cuối hợp đồng thông minh? Điều này cho hợp đồng thông minh biết tính thanh khoản của msg.sender trong nhóm, trừ đi phí đốt. Dòng mã sau đây gọi transfer() trên message.sender .

Do đó, một hợp đồng độc hại có thể liên tục yêu cầu rút tiền trước khi số tiền được cập nhật, mang lại cho họ tùy chọn rút gần như tất cả thanh khoản trong nhóm.

Luồng thông thường của một cuộc tấn công như vậy là như sau:

  • Hợp đồng dễ bị tổn thương có 10 ETH.
  • Kẻ tấn công gọi tiền gửi và gửi 1 eth.
  • Kẻ tấn công yêu cầu rút 1 eth và chức năng rút tiền thực hiện một số kiểm tra tại thời điểm này:
  • Kẻ tấn công có 1 ETH trong tài khoản của họ không? Đúng.
  • Chuyển 1 eth vào hợp đồng độc hại. LƯU Ý: Số dư của hợp đồng không thay đổi vì chức năng vẫn đang được thực thi.
  • Kẻ tấn công gọi lại để rút 1 eth. (vào lại)
  • Kẻ tấn công có 1 ETH trong tài khoản của họ không? Đúng.

Điều này sẽ lặp lại cho đến khi không còn thanh khoản trong nhóm.

Sự cố này trong ngôn ngữ Vyper đã được khắc phục và không còn tồn tại sau phiên bản 0.3.0. Nếu bạn là nhà phát triển hoặc tổ chức Web3 sử dụng Vyper, hãy đảm bảo cập nhật phiên bản của bạn ngay bây giờ.

Có thể bạn quan tâm