4주차 공부 - 2

박세연·2021년 1월 21일
0

Mastering Ethereum

목록 보기
5/10
post-thumbnail

Chapter 9-2. 스마트 컨트랙트 보안



💡 확인되지 않은 CALL 반환 값

✔️ 솔리디티에서 외부 호출을 수행하는 함수 중, callsend 함수는 호출 성공 또는 실패 여부에 따라 true, false를 반환한다.
✔️ 여기서 주의할 점은 함수 호출에 실패하는 경우, 단순히 false를 반환하기만 할 뿐 함수를 실행한 트랜잭션으로 돌아가는 것이 아니다.
✔️ 보통의 개발자는 외부 호출을 실패하게 되면 원래의 상태로 되돌아간다고 생각한다.

취약점


✅ 위 컨트랙트는 winnerwinAmount만큼의 이더를 받는 컨트랙트이다.
✅ 14행에서 send함수를 실행했을 때 트랜잭션에 실패해서 false를 반환하더라도 다음행에서 payedOut변수의 값이 true로 설정된다.
✅ 이렇게 payedOut의 값이 true가 된다면 withdraeLeftOver함수를 통해 컨트랙트의 잔액(상금)을 출금할 수 있게 된다.

예방 기법

transfer함수는 트랜잭션이 실패할 경우 롤백되므로 send함수보다는 transfer함수를 사용하는 것이 좋다.


💡 레이스 컨디션 / 프런트 러닝

✔️ 레이스 컨디션(race condition) : 경쟁 조건을 의미하며 사용자들이 코드의 실행을 경쟁한다는 의미
✔️ 재진입성은 레이스 컨디션의 한 종류이다.

취약점

✅ 이더리움 노드는 트랜잭션 풀에서 트랜잭션을 선택해서 블록을 생성한다.
✅ 채굴자가 작업 증명 알고리즘을 통해 해시 값을 찾고 해당 트랜잭션이 포함된 블록을 채굴해야만 트랜잭션은 유효한 것으로 간주된다.
✅ 채굴자는 트랜잭션 풀에서 블록에 포함시킬 트랜잭션을 임의로 선택할 수 있다. (수수료 순 등...)
✅ 이때 공격자는 트랜잭션 풀에서 채굴자들이 푸는 문제에 대한 답, 논스 값을 포함하는 트랜잭션 정보를 얻을 수 있다.
✅ 논스 값을 찾은 채굴자의 권한을 취소하거나 트랜잭션 상태를 공격자가 변경할 수 있다.


✅ 특정 해시값을 도출하는 solution을 찾으면 그 값을 찾은 사람은 1000이더를 받을 수 있는 컨트랙트이다.
✅ 만약 위 컨트랙트에서 solution값이 Ethereum! 이라면 이 값을 solve함수의 파라미터로 전달해서 이더를 받는다.
✅ 공격자는 바로 이 솔루션을 트랜잭션 풀에서 볼 수 있다.
✅ solution을 본 공격자는 해당 트랜잭션 보다 가스 가격이 높은 동일한 트랜잭션을 호출하면, 이 가스 값이 높은 트랜잭션이 채굴자에 의해 선택될 가능성이 높으므로 먼저 블록에 포함될 것이다.
✅ 공격자의 해당 트랜잭션이 블록에 포함되면 원래 문제를 풀었던 사람은 이더를 받지 못하고 공격자가 1000이더를 받게 된다.

예방 기법

  • 위와 같은 공격을 프런트 러닝(front-running)이라고 부르는데, 이 공격을 수행할 수 있는 사람은 (트랜잭션의 gasPrice를 수정하는)사용자(블록에서 트랜잭션을 재정렬할 수 있는)채굴자이다.
  1. 첫번째 방법은 가스 가격에 상한을 둬서 gasPrice를 높여서 트랜잭션 우선순위를 갖는 것을 방지한다. 이 방법은 채굴자의 공격은 막을 수 없다.
  2. 두번째 방법은 커밋-공개 방식을 사용하는 것이다. 트랜잭션을 보낼 때는 정보를 숨긴채 전송하고, 해당 트랜잭션이 블록에 포함되면 데이터를 표시하는 트랜잭션을 다시 전송하는 것이다. 이 방법은 채굴자와 사용자가 트랜잭션의 내용을 결정할 수 없기 때문에 프런트 러닝 트랜잭션을 방지할 수 있다.

💡 서비스 거부(DoS)

사용자가 일정 기간 또는 영구적으로 컨트랙트를 실행할 수 없게 만드는 공격

취약점

컨트랙트가 동작하지 않게 할 수 있는 다양한 방법

1. 외부에서 조작된 매핑 또는 배열을 통한 루핑

인위적으로 부풀려질 수 있는 배열을 사용해서 배열 크기르 크게 만들면 for루프를 실행하는데 요구되는 가스가 가스 한도를 초과하게 된다. 그렇게 되면 함수가 원하는대로 동작하지 않게 만들 수 있다.

2. 소유자 운영

컨트랙트의 기능을 사용하기 위해서 소유자의 작업이 필요한 경우, 권한을 가진 사용자가 개인키를 잃게 되면 컨트랙트를 사용할 수 없게 된다.

3. 외부 호출을 기반으로 한 진행 상태

이더를 보내야 되는 조건을 갖는 경우에, 사용자가 이더의 수신을 허용하지 않는 컨트랙트를 만들 수 있다. 이런 컨트랙트는 외부 호출을 실패하게 하거나 차단해서 컨트랙트가 새로운 상태를 달성하지 못하게 한다.

예방 기법

1. 외부에서 조작된 매핑 또는 배열을 통한 루핑

루프의 조건에 외부 사용자가 인위적으로 조작할 수 있는 변수를 사용하면 안된다.

2. 소유자 운영

  1. 첫 번째 해결책은 소유자를 다중 컨트랙트로 만든다.
  2. 두 번째 해결책은 시간-잠금을 사용한다. 컨트랙트 기능을 사용하기 위한 조건에 지정된 시간 이후에는 컨트랙트에 대한 요청이 소유자의 작업 없이도 실행되도록 설정한다.
    require(msg.sender == owner || now > unlockTime)

3. 외부 호출을 기반으로 한 진행 상태

시간-잠금 해결책을 이 경우에도 사용할 수 있다. 외부 호출이 필요한 경우에서 실패하거나 호출이 일어나지 않은 경우에 특정 시간 이후 자동으로 상태를 진행시킬 수 있도록 컨트랙트를 작성하는 방법이 있다.


💡 블록 타임스탬프 조작

채굴자는 블록의 타임스탬프를 조정할 수 있는데, 이로 인해서 스마트 컨트랙트에서 블록 타임스탬프를 잘못 사용하면 문제가 발생할 수 있다.

취약점


✅ 채굴자는 필요한 경우 block.Timestamp와 그 별칭인 now를 조작할 수 있다.
✅ 컨트랙트의 잔액을 받는 조건을 달성하기 위해 채굴자는 타임스탬프를 조정할 수 있다.

예방 기법

블록 타임스탬프를 컨트랙트에서 특정 조건 체크를 위한 요소로 사용하면 안된다.
하지만 시간과 관련된 조건이 필요한 경우가 있다. 예를 들어 컨트랙트가 해지되는 시간, 만기일 적용과 같은 시간 관련 조건이 있을 경우에는 블록의 타임스탬프를 사용하지 말고 block.number과 평균 블록 시간을 사용해서 설정하는 것이 좋다.
✅ 이더리움은 10초당 하나의 블록이 생성되고 일주일이면 대략 60,480개의 블록이 생성된다.
✅ 블록 번호는 블록 타임스탬프와는 다르게 채굴자가 쉽게 조작할 수 없기 때문에 블록의 생성 시간과 블록 넘버를 사용하면 안전한 컨트랙트를 작성할 수 있다.


💡 생성자 관리

✔️ 생성자(constructor)는 컨트랙트를 생성할 때 호출되는 값으로, 컨트랙트를 초기화할 때 종종 중요하고 권한을 필요로 하는 작업을 수행하는 특수 함수
✔️ 솔리디티 v0.4.22 이전 버전에서는 생성자를 constructor로 정의하는 것이 아니라 컨트랙트와 이름이 같은 함수를 생성자로 정의했다.
✔️ 이러한 경우에 컨트랙트 이름이 변경되면 원래의 생성자는 생성자의 역할을 하지 못하고 일반 함수가 된다. 이 점이 취약점이 될 수 있다.

취약점

컨트랙트의 이름이 수정되거나 생성자의 이름을 잘못 지정하면 생성자는 일반 함수처럼 작동하게 된다.

✅ 해당 코드를 작성할 때 개발자는 생성자를 통해서 owner변수를 컨트랙트 소유자로 설정하는 것을 목적으로 했다.
✅ 하지만 생성자 함수를 보면 이름이 잘못되어 생성자가 아닌 일반 함수로서 작동한다.
✅ 따라서 컨트랙트의 소유자가 아닌 일반 사용자가 해당 함수를 호출해서 자신이 컨트랙트의 소유자가 되고, withdraw함수 호출을 통해 컨트랙트의 잔액을 자신의 계좌로 송금할 수 있다.

예방 기법

✅ 솔리디티 컴파일러 v0.4.22 이후부터는 constructor키워드를 사용하므로 위와 같은 문제를 방지할 수 있다.


💡 초기화되지 않은 스토리지 포인터

EVM은 스토리지(storage)와 메모리(memory)에 변수, 데이터를 저장한다.
✔️ 스토리지(storage) : 블록체인 상에 영구적으로 저장되는 변수
✔️ 메모리(memory) : 임시적으로 저장되는 변수. 컨트랙트 함수에 대한 외부 호출들이 일어나는 사이에 지워질 수 있다.
솔리디티는 보통 상태 변수(함수 외부에 선언되는 변수)는 storage로 초기화하고, 함수 내에 선언된 변수는 memory로 자동으로 초기화한다.

취약점

변수를 부적절하게 초기화하면 취약한 컨트랙트를 생성할 수 있다.
✅ 로컬 스토리지 변수를 초기화하지 않으면 컨트랙트의 다른 스토리지 변숫값이 포함될 수 있다. 이 부분이 취약점을 유발하거나 악용될 수 있다.

예방 기법

✅ 솔리디티 컴파일러는 초기화되지 않은 스토리지 변수에 대한 경고를 보여준다.
✅ 스마트 컨트랙트를 작성할 때 memory 혹은 storage 지정자를 명시적으로 사용하는 것이 좋다.


💡 부동소수점 및 정밀도

솔리디티에서 고정소수점, 부동소수점은 아직 완벽하게 지원하지 않는다. 솔리디티 버전 0.5.10 참고

취약점

✅ 솔리디티에서는 정수 데이터 타입을 사용해서 부동소수점 표현을 해야한다.

✅ 10행, 16행의 나누기 연산 같은 경우에 정수 데이터 타입으로 연산을 하므로 버려지는 값이 존재한다.(정확한 연산 불가)

예방 기법

✅ 나누기 연산을 할 때 더 큰 수로 나누는 방법을 사용한다. (분수에서 분자에 더 큰 수를 사용한다.)
✅ 연산 순서를 조정한다. (위의 예시에서는 곱셈 후 나눗셈 수행을 한다면 더 정확한 결과를 얻을 수 있다.)
✅ 숫자에 대해 임의의 정밀도를 정의할 때, 높은 정밀도로 변환하고 수학 연산을 수행하고 출력에 필요한 정밀도로 다시 변환한다. (일반적으로 가스사용에 최적인 uint256을 사용한다.


💡 Tx.Origin 인증

✔️ 글로벌 변수 tx.origin : 트랜잭션의 발신자를 나타낸다. msg.sender와 다른 점은 전체 호출을 가로지르고 원래 호출을 보낸 계정의 주소를 나타낸다.

취약점

tx.origin 변수를 사용하여 사용자에게 권한을 부여하는 컨트랙트는 일반적으로 사용자에게 컨트랙트 인증 작업을 수행하도록 속이는 공격에 취약하다.

withdrawAll함수는 tx.origin을 사용해 함수 사용을 승인한다. 즉 컨트랙트의 소유자만 해당 함수를 실행할 수 있다.

✅ 공격자는 공격 대상자(Phishable컨트랙트의 소유자)에게 컨트랙트를 위장해서 AttackContract를 호출하게 한다.
✅ 공격 대상자(피해자)가 AttackContract 컨트랙트에 트랜잭션을 보내면 다른 함수가 없기 때문에 자동으로 폴백함수가 호출된다.
withdrawAll함수의 파라미터는 attacker로 공격자의 주소이며, 이 컨트랙트에 트랜잭션을 보낸 tx.origin은 공격 대상자, 즉 Phishable컨트랙트의 소유주를 의미하기 때문에 withdrawAll함수를 실행할 수 있게 되고 컨트랙트의 잔액을 공격자의 주소로 넘겨주게 된다.

예방 기법

✅ 스마트 컨트랙트에서 권한을 위해 트랜잭션을 보낸 계정의 주소를 사용할 때는 tx.origin을 사용하면 안된다.


profile
안녕하세요

0개의 댓글