Integer Overflow and Underflow
原理
EVM的整数有int和uint两种。
在 Solidity
语言中,变量支持的整数类型步长是以8递增的,从 uint8
到 uint256
, uint
默认是 uint256
,以 uin8
为例
我们知道 uint8
是8位,我们最多可以 2**8-1
,也就是 255,若是256则会造成溢出,这是上溢
下溢也是一样的, uint(0)-1
就是255
例子
Capture The Ether 的 Token sale
pragma solidity ^0.4.21;
contract TokenSaleChallenge {
mapping(address => uint256) public balanceOf;
uint256 constant PRICE_PER_TOKEN = 1 ether;
function TokenSaleChallenge(address _player) public payable {
require(msg.value == 1 ether);
}
function isComplete() public view returns (bool) {
return address(this).balance < 1 ether;
}
function buy(uint256 numTokens) public payable {
require(msg.value == numTokens * PRICE_PER_TOKEN);
balanceOf[msg.sender] += numTokens;
}
function sell(uint256 numTokens) public {
require(balanceOf[msg.sender] >= numTokens);
balanceOf[msg.sender] -= numTokens;
msg.sender.transfer(numTokens * PRICE_PER_TOKEN);
}
}
分析:
首先规定了合约中有1 ether,一个代币需要支付1 ether. 而isComplete()要求余额小于1 ether。
buy(uint256 numTokens)函数则是msg.value == numTokens * PRICE_PER_TOKEN。
再EVM里1ether=10* *8 wei。然而EVM虚拟机最大只有256位即 2**256-1.因此只要我们的numTokens是一个很大的值,就会溢出。即我们花费不足1etner就买到了大量的代币,将一些代币卖出即可完成题目要求。
因此,我们可以购买2**256//10* *18+1 个代币就可以完成题目要求。
整数下溢
contract Bank {
mapping(address => uint256) public balanceOf;
...
function withdraw(uint256 amount) public {
require(balanceOf[msg.sender] - amount >= 0);
balanceOf[msg.sender] -= amount;
msg.sender.send.value(amount)();
}
}
分析:
由于uint为无符号类型,因此
balanceOf[msg.sender] - amount >= 0看似没有任何问题,但是由于都是无符号类型,因此结果永远大于等于0的。所以我们可以任意取款。应改为balanceOf[msg.sender] >=amount
还有一种情况,与重入攻击有关:将1的物品卖出两次导致下溢为巨大的正数。
题目
【ciscn2019】 Daysbank
pragma solidity ^0.4.24;
contract DaysBank {
mapping(address => uint) public balanceOf;
mapping(address => uint) public gift;
address owner;
constructor()public{
owner = msg.sender;
}
event SendFlag(uint256 flagnum, string b64email);
function payforflag(string b64email) public {
require(balanceOf[msg.sender] >= 10000);
emit SendFlag(1,b64email);
}
function getgift() public{
require(gift[msg.sender]==0);
balanceOf[msg.sender]+=1;
gift[msg.sender]=1;
}
function transfer(address towhere, uint howmuch) public {
require(howmuch>1);
require(balanceOf[msg.sender]>1);
require(balanceOf[msg.sender]>=howmuch);
balanceOf[msg.sender]-=howmuch;
balanceOf[towhere]+=howmuch;
}
function profit() public{
require(balanceOf[msg.sender]==1);
require(gift[msg.sender]==1);
balanceOf[msg.sender]+=1;
gift[msg.sender]=2;
}
function transfer2(address towhere, uint howmuch) public {
require(howmuch>2);
require(balanceOf[msg.sender]>2);
require(balanceOf[msg.sender]-howmuch>0);
balanceOf[msg.sender]-=howmuch;
balanceOf[towhere]+=howmuch;
}
}
分析:
找到flag的函数payforflag(),观察得到,要想得到flag需要balanceof大于10w。
而transfer2()函数中balanceOf[msg.sender]-howmuch>0存在整数下溢的漏洞,因而可以利用。但是需要满足balanceOf[msg.sender]>2的要求。
我们可以通过getgift()来获得一个代币,然后可以满足profit()的要求,从而获得两个代币 ,此时balanceOf为2,gift为1。如果要达到balanceOf[msg.sender]>2,那么需要利用transfer()函数才能达到要求,transfer没有下溢的漏洞。
攻击过程:
先利用账号a,通过getgift(),然后利用profit()函数,此时余额为2,gift为1.
然后再用账号b,重复该操作。
再将账号a利用transfer函数转给账号b两个代币。
然后再利用账号b调用transfer2转给账号a一个非常大的金额,达到溢出的效果,此时两个地址都可以执行flag函数。