一、常见漏洞介绍:
智能合约是运行在区块链上的自动执行代码。由于它们的不可逆性和与金钱相关的特性,智能合约的安全问题受到了广泛关注。以下是一些常见的智能合约漏洞:
-
整数溢出与下溢:当整数变量的值超出其允许的范围时,可能会发生溢出或下溢,导致不可预测的结果。
-
重入攻击:在一个函数调用中,被调用的合约再次调用原函数,可能导致不希望的多次执行。例如,著名的DAO攻击就是重入攻击的一种。
-
短时间浮动:在区块链上,时间和块高度可以被操纵。攻击者可以利用这点,通过挖矿操作来操纵合约的时间逻辑。
-
随机性问题:在以太坊等区块链平台上,产生随机数是有挑战的,因为所有信息都是公开的。攻击者可能预测或操纵随机结果。
-
委托调用:使用
delegatecall
或其他低级调用可能导致合约中的状态被意外修改。 -
异常处理不当:如果合约在遇到异常时没有正确处理,可能导致资金丢失或被锁定。
-
未初始化的存储指针:如果合约使用了未初始化的存储指针,可能会导致存储被意外修改。
-
控制流可预测性:如果攻击者可以预测或操纵合约的控制流,他们可能会利用这点执行恶意操作。
-
前端/后端不匹配:智能合约的后端代码和前端应用的逻辑可能存在不匹配,导致用户资金损失。
-
权限过于宽松:如果合约的权限控制设置得过于宽松,攻击者可能利用这点执行不应允许的操作。
扫描二维码关注公众号,回复: 17334054 查看本文章 -
固定合约逻辑:由于智能合约一旦部署就无法更改,任何发现的漏洞都无法直接修复。需要有升级机制来处理此问题。
二、整数溢出漏洞分析:
1.整数溢出类型:
- 乘法溢出
- 加法溢出
- 减法溢出
- uint8到uint256,以及int8到int256
2..整数溢出原理分析:
溢出举例:
Example 1:
function withdraw(uint _amout) {
require(balances[msg.sender] - _amout > 0);
msg.sender.transfer(_amout);
balances[msg.sender] -= _amout;
}
balances[msg.sender]
和_amout
都是无符号的int型本身不存在小于0的情况,这里的两个值只要不相等,判断就会成立,就可以通过第四行达到一个减法下溢。
Example 2:
function popArrayOfThings() {
require(arrayOfThings.length >= 0);
arrayOfThings.length--;
}
当数组arrayOfThings
长度为0时,同样会造成一个减法下溢,从而导致可以以此来控制其他地方的变量。
Example 3:
for (var i = 0; i < somethingLarge; i++){
// ...
}
var
是一个uint8
类型,假设somethingLarge
是一个uint256
类型的值,那么这里的i
永远不会超过256.
防止措施:
加法溢出:
function safeAdd(uint256 a,uint256 b) internal
returns (uint256){
uint256 c = a + b;
assert(c>=a && c>=b);
return c;
}
减法溢出:
function safeSub(uint256 a,uint256 b) internal
returns (uint256){
assert(b <= a);
return a - b;
}
乘法溢出:
function safeMul(uint256 a,uint256 b) internal
returns (uint256){
uint256 c = a * b;
assert(a == 0 || c / a == b);
return c;
}
3.整出溢出漏洞实例分析:
将题目部署到测试链上面后,审一下代码:
pragma solidity >=0.4.22 <0.6.0;
contract Vul {
mapping (address => uint) balances; //定义了一个键值对,变量名为balances
// 一个钱包地址对应一个账户余额
event sendflag(string b64email);
function payforflag(string memory b64email) public{ //花钱购买flag
require(balances[msg.sender] >= 10000000000);
balances[msg.sender]=0;
emit sendflag(b64email);
}
function balanceOf() view public returns (uint) { //返回当前账户的余额
return balances[msg.sender];
}
function deposit() public payable { //当前账户余额加一
balances[msg.sender] += 1;
}
function withdraw(uint _amount) public { //从当前账户取钱(_amount变量表示取多少)
require(balances[msg.sender] - _amount > 0); //require用于确认条件的有效性
balances[msg.sender] -= _amount;
}
}
分析代码可以知道。flag需要用币买下来,但是我们要花10000000000及以上的币才能买到flag,如果一个币一个币的申请将会十分耗时,不知道要猴年马月才能买下flag。仔细观察withdraw
函数是存在一个整数下溢的漏洞的,变量_amount
和balances[msg.sender]
都是uint256
类型,所以不存在负数,就没有小于0这一说,当我们的余额小于要取出的余额就会导致最后我们的余额超级翻倍,这里可以先获得一个币,再取两个币。这样根据uint类型的一个回还,最终我们的账户就有了2^256-1个币。
操作一下通过bavanceOf
函数查看余额:
正好是2^256-1,此时就可以购买flag了