Cảnh báo rủi ro: Đề phòng huy động vốn bất hợp pháp dưới danh nghĩa 'tiền điện tử' và 'blockchain'. — Năm cơ quan bao gồm Ủy ban Giám sát Ngân hàng và Bảo hiểm
Tìm kiếm
Đăng nhập
简中
繁中
English
日本語
한국어
ภาษาไทย
Tiếng Việt
BTC
ETH
HTX
SOL
BNB
Xem thị trường
Tìm hiểu sâu về cách một cuộc tấn công quay trở lại đã đánh cắp 70 triệu đô la từ nhóm của Curve
jk
Odaily资深作者
2023-08-07 00:58
Bài viết này có khoảng 2222 từ, đọc toàn bộ bài viết mất khoảng 4 phút
Khoa học kỹ thuật về các cuộc tấn công reentrancy.

nguyên bảnA Deep Dive Into How Curve Pool’s $ 70 Million Reentrancy Exploit Was Possiblenguyên bản

, Tác giả: Ann, biên soạn bởi Odaily jk.

Vụ vi phạm Curve pool gần đây khác với hầu hết các vụ hack tiền điện tử mà chúng ta đã 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ụ 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à là tùy thuộc vào nó. 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: 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 nguyên nhân đằng sau lỗ hổng này nên tôi quyết định tìm hiểu sâu hơn.

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 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 cụ thể này của Vyper đều có thể trở thành mục tiêu.

danh hiệu cấp một

Sự tái nhập là gì?

Để hiểu lý do tại sao lỗ hổng này xảy ra, trước tiên chúng ta cần hiểu reentrancy là gì và nó hoạt động như thế nào.

Một hàm được cho là được thực hiện lại nếu nó có thể bị gián đoạn trong quá trình thực thi và có thể được gọi lại một cách an toàn (tái nhập) trước khi lệnh gọi trước đó của nó thực hiện xong. Các hàm quay lại được sử dụng trong các ứng dụng như xử lý ngắt phần cứng và đệ quy.

  • Để một hàm được vào lại, nó cần phải đáp ứng các điều kiện sau:

  • Nó không thể sử dụng dữ liệu toàn cầu và tĩnh. Đây chỉ là quy ước, không có giới hạn cứng nhưng có thể mất thông tin nếu một chức năng sử dụng dữ liệu toàn cục bị gián đoạn và khởi động lại.

  • Nó không nên sửa đổi mã riêng của nó. Bất cứ khi nào một chức năng bị gián đoạn, nó sẽ có thể thực hiện theo cách tương tự. Đ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 được gửi lại khác. Không nên nhầm lẫn Reentrancy với thread safety mặc dù chúng có liên quan chặt chẽ với nhau. Một chức năng có thể an toàn cho luồng và vẫn không được cấp lại. Để tránh nhầm lẫn, việc quay lại chỉ liên quan đến một luồng thực thi. Đây là một khái niệm vào thời điểm chưa có hệ điều hành đa nhiệm nào tồn tại.

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

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

  • Chức năng 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 mà tô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 đá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í.

danh hiệu cấp một

Khóa là gì?

Khóa về cơ bản là một cơ chế đồng bộ hóa luồng, theo đó một quy trình có thể yêu cầu hoặc khóa một quy trình khác.

Loại khóa đơn giản nhất được gọi là semaphore nhị phân. Khóa này cung cấp quyền truy cập độc quyền vào dữ liệu bị khóa. Ngoài ra còn có các loại khóa phức tạp hơn cung cấp quyền truy cập chung để đọc dữ liệu. Việc sử dụng sai khóa trong lập trình có thể dẫn đến deadlock hoặc livelock, trong đó các tiến trình liên tục chặn lẫn nhau, thay đổi trạng thái mà không tiến triển.

@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

Ngôn ngữ lập trình sử dụng các khóa ở hậu trường để quản lý và chia sẻ các thay đổi trạng thái giữa nhiều chương trình con một cách linh hoạt. Tuy nhiên, một số ngôn ngữ, chẳng hạn như C# và Vyper, cho phép sử dụng khóa trực tiếp trong mã.

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() mà không có khóa, msg.sender có thể gọi tất cả mã ở trên trước khi hàm của chúng ta 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 nhằm ngăn người gọi thực hiện nhiều lần các chức năng hợp đồng thông minh trước khi họ 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 hại đã 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ều độc đáo trong 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 đều 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.

Chúng ta hãy xem xét một hợp đồng thực sự dễ bị tấn công trở lại. Lưu ý công cụ sửa đổi @nonreentrant('lock')? Thông thường điều này sẽ ngăn cản việc quay trở lại, nhưng thực tế không phải vậy. Kẻ tấn công có thể gọi liên tục 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 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

danh hiệu cấp một

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

Cho đến nay, chúng ta biết rằng cuộc tấn công reentrancy là một phương pháp gọi đi gọi lạ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 lại dẫn đến việc đánh cắp tiền và mất 70 triệu USD trong vụ tấn công Curve?

Bạn có để ý self.balanceOf[msg.sender] -= _burn_amount ở cuối hợp đồng thông minh không? Đ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í ghi. Dòng mã sau 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 gần như rút toàn bộ thanh khoản trong nhóm.

  • Quy trình thông thường của một cuộc tấn công như vậy 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 thực thi.

  • Kẻ tấn công gọi lại để rút 1 eth. (nhập 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.

Curve
Chào mừng tham gia cộng đồng chính thức của Odaily
Nhóm đăng ký
https://t.me/Odaily_News
Tài khoản chính thức
https://twitter.com/OdailyChina