

원래의A Deep Dive Into How Curve Pool’s $ 70 Million Reentrancy Exploit Was Possible원래의
, 저자: Ann, Odaily jk 편집.
최근의 Curve 풀 위반은 지난 몇 년간 우리가 보아온 대부분의 암호화폐 해킹과 다릅니다. 이전의 많은 위반과 달리 이번 위반은 스마트 계약 자체의 취약점과 직접적인 관련이 없었지만 그에 따른 종속 문제였습니다. 사용되는 언어의 기본 컴파일러에 대해.
여기서는 EVM(Ethereum Virtual Machine)과 인터페이스하도록 설계된 스마트 계약용 Python 프로그래밍 언어인 Vyper에 대해 이야기하고 있습니다. 저는 이 취약점 뒤에 숨어 있는 것이 무엇인지에 매우 관심이 있었기 때문에 더 자세히 알아보기로 결정했습니다.
이 취약점은 재진입 버그라고 하며 일부 버전의 Vyper 프로그래밍 언어, 특히 v 0.2.15, v 0.2.16 및 v 0.3.0에 존재합니다. 따라서 이러한 특정 버전의 Vyper를 사용하는 모든 프로젝트가 대상이 될 수 있습니다.
첫 번째 수준 제목
재진입이란 무엇입니까?
이 취약점이 발생한 이유를 이해하려면 먼저 재진입이 무엇인지, 어떻게 작동하는지 이해해야 합니다.
함수가 실행 중에 중단될 수 있고 이전 호출이 실행을 완료하기 전에 안전하게 다시 호출(재진입)될 수 있는 경우 함수를 재진입 가능하다고 합니다. 재진입 기능은 하드웨어 인터럽트 처리 및 재귀와 같은 응용 프로그램에 사용됩니다.
함수가 재진입 가능하려면 다음 조건을 충족해야 합니다.
전역 및 정적 데이터를 사용할 수 없습니다. 이는 단지 관례일 뿐이며 엄격한 제한은 없지만 전역 데이터를 사용하는 기능이 중단되었다가 다시 시작되면 정보가 손실될 수 있습니다.
자체 코드를 수정해서는 안 됩니다. 함수가 중단될 때마다 동일한 방식으로 실행될 수 있어야 합니다. 이는 관리할 수 있지만 일반적으로 권장되지 않습니다.
재진입이 불가능한 다른 함수를 호출하면 안 됩니다. 재진입은 스레드 안전성과 밀접하게 관련되어 있지만 혼동해서는 안 됩니다. 함수는 스레드로부터 안전하면서도 재진입이 불가능할 수 있습니다. 혼란을 피하기 위해 재진입에는 단 하나의 실행 스레드만 포함됩니다. 이는 멀티태스킹 운영체제가 존재하지 않던 시절의 개념이었습니다.
i = 5
def non_reentrant_function():
return i** 5
def reentrant_function(number:int):
return number** 5
실제적인 예는 다음과 같습니다.
non_reentrant_function 함수:
이 함수에는 매개변수가 없습니다.
이 함수는 i의 5제곱을 직접적으로 전역 변수로 반환합니다.
따라서 이 함수를 호출하면 항상 5** 5 , 즉 3125 가 반환됩니다.
함수 reentrant_function:
이 함수에는 정수인 매개변수 번호가 있습니다.
5제곱한 인수 번호를 반환합니다.
많은 스마트 계약 기능은 지갑 잔액과 같은 글로벌 정보에 액세스하기 때문에 재진입이 불가능하다는 점에 주목할 가치가 있습니다.
첫 번째 수준 제목
자물쇠란 무엇입니까?
잠금은 본질적으로 한 프로세스가 다른 프로세스를 요구하거나 잠글 수 있는 스레드 동기화 메커니즘입니다.
가장 간단한 유형의 잠금을 이진 세마포어라고 합니다. 이 잠금은 잠긴 데이터에 대한 독점적인 액세스를 제공합니다. 데이터 읽기에 대한 공유 액세스를 제공하는 더 복잡한 유형의 잠금도 있습니다. 프로그래밍에서 잠금을 잘못 사용하면 프로세스가 지속적으로 서로를 차단하고 진행 없이 상태가 변경되는 교착 상태 또는 라이브 잠금이 발생할 수 있습니다.
@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에서 비재진입('잠금') 데코레이터는 실행이 완료되기 전에 호출자가 스마트 계약 기능을 반복적으로 실행하는 것을 방지하기 위해 기능에 대한 액세스를 제어하는 메커니즘입니다.
많은 DeFi 해킹에서는 일반적으로 계약 개발자가 예상하지 못한 스마트 계약 오류이며, 영리하지만 악의적인 공격자가 일부 기능이나 데이터가 노출되는 방식에서 약점을 발견했습니다. 하지만 이 사례에서 특이한 점은 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
첫 번째 수준 제목
이것은 어떻게 악용됩니까?
지금까지 우리는 재진입 공격이 스마트 계약에서 특정 기능을 반복적으로 호출하는 방법이라는 것을 알고 있었습니다. 그러나 이것이 어떻게 Curve 공격으로 자금을 도난당하고 7천만 달러의 손실을 입혔습니까?
스마트 계약이 끝나면 self.balanceOf[msg.sender] -= _burn_amount가 표시되나요? 이는 소각 수수료를 제외하고 풀에 있는 msg.sender의 유동성을 스마트 계약에 알려줍니다. 다음 코드 줄은 message.sender 에서 transfer()를 호출합니다.
따라서 악의적인 계약은 금액이 업데이트되기 전에 지속적으로 인출을 호출하여 풀의 모든 유동성을 거의 인출할 수 있는 옵션을 제공할 수 있습니다.
이러한 공격의 일반적인 흐름은 다음과 같습니다.
취약한 계약에는 10개의 이더가 있습니다.
공격자는 예금을 요청하고 1 eth를 예치합니다.
공격자는 1 eth 출금을 요청하고 이때 출금 기능은 몇 가지 검사를 수행합니다.
공격자의 계정에 1ETH가 있습니까? 예.
1 eth를 악의적인 계약으로 전송합니다. 참고: 기능이 계속 실행 중이므로 계약의 잔액은 변경되지 않았습니다.
공격자는 1ETH를 인출하기 위해 다시 호출합니다. (재입장)
공격자의 계정에 1ETH가 있습니까? 예.
이는 풀에 더 이상 유동성이 없을 때까지 반복됩니다.
