이 글은 컴파일 과정에서 solidity(0.8.13<=solidity<0.8.17) 컴파일러의 소스 코드 수준에서 Yul 최적화 메커니즘의 결함으로 인해 상태 변수 할당 작업이 실수로 삭제되는 것을 자세히 설명합니다. 지침.
첫 번째 레벨 제목
1. 취약점 세부 정보
공식 문서공식 문서。
컴파일 프로세스의 UnusedStoreEliminator 최적화 단계에서 컴파일러는 "중복" 스토리지 쓰기 작업을 제거하지만 "중복"의 식별 결함으로 인해 Yul 함수 블록이 특정 사용자 정의 함수(함수)를 호출할 때 호출 블록의 실행 흐름에 영향을 미치지 않는 내부 특정 분기), Yul 함수 블록에서 호출된 함수 전후에 동일한 상태 변수에 대한 쓰기 작업이 있어 블록에서 사용자 정의 함수가 발생합니다. Yul 최적화 메커니즘에 의해 호출되기 전에저장소 쓰기 작업은 컴파일 수준에서 영구적으로 삭제됩니다.。
다음 코드를 고려하십시오.
contract Eocene {
uint public x;
function attack() public {
x = 1;
x = 2;
}
}
x=1은 UnusedStoreEliminator가 최적화할 때 attack() 함수의 전체 실행에 분명히 중복됩니다. 당연히 최적화된 Yul 코드는 계약의 가스 소비를 줄이기 위해 x=1;을 삭제합니다.
다음으로 사용자 정의 함수에 대한 호출을 중간에 삽입하는 것을 고려하십시오.
contract Eocene {
uint public x;
function attack(uint i) public {
x = 1;
y(i);
x = 2;
}
function y(uint i) internal{
if (i > 0){
return;
}
assembly { return( 0, 0) }
}
}
분명히 y() 함수의 호출로 인해 y() 함수가 전체 함수 실행 흐름을 종료( 롤백이 아니라는 점에 유의하십시오(Yul 코드 return() 함수). 그런 다음 x= 1은 분명히 삭제할 수 없으므로 위 계약의 경우 y() 함수에 {return(0, 0)} 어셈블리가 존재합니다. 전체 메시지 호출이 종료될 수 있으므로 x=1은 당연히 삭제할 수 없습니다.
그러나 Solidity 컴파일러에서는 코드 논리 문제로 인해 x=1이 컴파일 중에 실수로 삭제되어 코드 논리가 영구적으로 변경되었습니다.
실제 컴파일 테스트 결과는 다음과 같습니다.
충격! 최적화해서는 안 되는 x=1인 Yul 코드가 손실됩니다! 다음에 무슨 일이 있었는지 알고 싶다면 아래를 읽으십시오.
견고한 컴파일러 코드의 UnusedStoreEliminator에서 SSA 변수 추적 및 제어 흐름 추적을 사용하여 스토리지 쓰기 작업이 중복되는지 여부를 확인합니다. 사용자 지정 함수를 입력할 때 UnusedStoreEliminator가 발생하면 다음이 발생합니다.
메모리 또는 스토리지 쓰기 작업: 메모리 및 스토리지 쓰기 작업을 m_store 변수에 저장하고 작업의 초기 상태를 미정으로 설정합니다.
함수 호출: 함수의 메모리 또는 스토리지 읽기 및 쓰기 작업 위치를 가져오고 이를 m_store 변수에 저장된 미결정 상태의 모든 작업과 비교합니다.
1. m_store에서 저장 작업의 쓰기 덮어쓰기인 경우 m_store에서 해당 작업 상태를 Unused로 변경합니다.
2. m_store에서 저장 작업의 읽기인 경우 해당 m_store에서 해당 작업 상태를 사용됨으로 변경합니다.
3. 함수에 메시지 호출을 계속 실행할 수 있는 분기가 없으면 m_store의 모든 메모리 쓰기 작업을 Unused로 변경합니다.
1. 어필 조건에서 함수가 실행 흐름을 종료할 수 있는 경우 상태가 m_store에서 미결정인 스토리지 쓰기 작업을 사용됨으로 변경하고, 그렇지 않으면 미사용으로 표시합니다.
기능 종료: 미사용으로 표시된 모든 쓰기 작업 삭제
메모리 또는 저장소에 쓰기 위한 초기화 코드는 다음과 같습니다.
발생한 메모리 및 스토리지 쓰기 작업이 m_store에 저장되는 것을 볼 수 있습니다.
함수 호출 발생 시 처리 논리 코드는 다음과 같습니다.
그 중 operationFromFunctionCall() 및 applyOperation()은 항소 2.1 및 2.2의 처리 논리를 구현합니다. 아래 canContinue 및 canTerminate 함수를 기반으로 판단되는 If 문은 2.3 논리를 구현합니다.
허점의 존재로 이어지는 것은 아래의 If 판단의 결함이라는 점에 유의해야 합니다! ! !
operationFromFunctionCall() 이 함수의 모든 메모리 또는 스토리지 읽기 및 쓰기 작업을 가져오려면 Yul에 sstore(), return()과 같은 많은 내장 함수가 있다는 점에 유의해야 합니다. 여기에서 내장 함수와 사용자 정의 함수에 대해 서로 다른 처리 논리가 있음을 알 수 있습니다.
applyOperation() 함수는 operationFromFuncitonCall()에서 얻은 모든 읽기 및 쓰기 작업을 비교하여 이 함수 호출에서 m_store에 저장된 데이터를 읽거나 쓰는지 확인하고 m_store에서 해당 작업 상태를 수정합니다.
Eocene 계약의 attack() 함수에서 위에서 언급한 UnusedStoreEliminator 최적화 로직의 처리를 고려하십시오.
x= 1을 m_store 변수에 저장하고 상태를 미결정으로 설정합니다.
1. y() 함수 호출 발생, y() 함수 호출의 모든 읽기 및 쓰기 작업 가져오기
2. m_store 변수를 순회하여 y() 호출로 인한 모든 읽기 및 쓰기 작업이 x=1과 관련이 없고 x=1의 상태가 여전히 미결정임을 확인합니다.
1. y() 함수에는 정상적으로 반환할 수 있는 분기가 있으므로 canContinue가 True이고 If 판단에 들어가지 않기 때문에 y() 함수의 제어 흐름 논리를 얻습니다. x= 1 상태는 아직 미정입니다! ! !
3. 발생한 x= 2 스토리지 작업:
1. m_store 변수를 순회하여 미결정 상태에서 x= 1, x= 2 연산이 x= 1을 재정의하고 x= 1의 상태를 미사용으로 설정함을 찾습니다.
2. x= 2 연산을 m_store에 저장하고 초기 상태는 미결정입니다.
4. 기능 종료:
1. 모든 m_store의 미결정 상태의 동작 상태를 사용됨으로 변경
2. m_store에서 미사용 상태의 모든 작업 삭제
분명히 함수가 호출될 때 호출된 함수가 메시지의 실행을 종료할 수 있다면 호출된 함수 이전의 미결정 상태의 모든 쓰기 작업은 미결정 상태로 남아 있는 대신 사용됨으로 변경되어야 합니다. 호출된 함수 Action이 실수로 삭제되었습니다.
존재하다
존재하다Solidity, 기본적으로 동일한 논리에서 영향을 받지 않는 계약 코드의 예입니다. 그러나 UnusedStoreEliminator의 처리 논리에 다른 가능성이 있기 때문이 아니라 UnusedStoreEliminator 이전의 Yul 최적화 단계에서 호출된 함수를 작거나 하나만 포함하는 FullInliner 최적화 프로세스가 있기 때문에 코드가 이 취약점의 영향을 받지 않습니다. call into the call 함수에서 취약성 트리거 조건의 사용자 정의 함수는 피합니다.
contract Normal {
uint public x;
function f(bool a) public {
x = 1;
g(a);
x = 2;
}
function g(bool a) internal {
if (!a)
assembly { return( 0, 0) }
}
}
컴파일 결과는 다음과 같습니다.
첫 번째 레벨 제목
2. 솔루션
가장 근본적인 해결책은 영향을 받는 범위에서 솔리디티 컴파일러를 사용하지 않고 컴파일하는 것이며, 취약한 버전의 컴파일러를 사용해야 하는 경우 컴파일 시 UnusedStoreEliminator 최적화 단계를 제거하는 것을 고려할 수 있습니다.
여러 최적화 단계의 복잡성과 실제 함수 호출 흐름의 복잡성을 고려하여 계약 코드 수준에서 취약점을 완화하려면 전문 보안 담당자를 찾아 코드 감사를 수행하여 이로 인해 발생하는 계약의 취약점을 발견하십시오. 취약점 보안 질문.
