以太坊CVE-2018-10468与CVE-2018-11411合约漏洞分析

admin 2025年6月10日18:09:25评论9 views字数 8434阅读28分6秒阅读模式

一、前言

最近在审计代码的时候发现了两则十分相似的代码,审计之后发现代码存在一些问题,后来查询发现两则代码确为2018年的CVE。

即:CVE-2018–11411与CVE-2018–10468。

由于者的漏洞点十分相似,所以在本文中我将两代码一同进行分析。

其代码分别位于:https://etherscan.io/address/0x27f706edde3aD952EF647Dd67E24e38CD0803DD6#codehttps://etherscan.io/address/0xf084d5bc3e35e3d903260267ebd545c49c6013d0#code

经过简单的分析,我发现两则漏洞均是由编写者在编写代码过程中错误撰写合约判断条件从而导致攻击者可以利用条件传入合适的参数从而绕过判断进而作恶。

代码并不长,但是危害性极大。

二、代码分析

由于代码十分相似,所以我们仅分析其不同点与关键点。

合约如下:

/** * Source Code first verified at https://etherscan.io on Wednesday, June 28, 2017 (UTC) */pragma solidity ^0.4.10;contract ForeignToken {    function balanceOf(address _owner) constant returns (uint256);    function transfer(address _to, uint256 _value) returns (bool);}contract UselessEthereumToken {    address owner = msg.sender;    bool public purchasingAllowed = false;    mapping (address => uint256) balances;    mapping (address => mapping (address => uint256)) allowed;    uint256 public totalContribution = 0;    uint256 public totalBonusTokensIssued = 0;    uint256 public totalSupply = 0;    function name() constant returns (string) { return "Useless Ethereum Token"; }    function symbol() constant returns (string) { return "UET"; }    function decimals() constant returns (uint8) { return 18; }    function balanceOf(address _owner) constant returns (uint256) { return balances[_owner]; }    function transfer(address _to, uint256 _value) returns (bool success) {        // mitigates the ERC20 short address attack        if(msg.data.length < (2 * 32) + 4) { throw; }        if (_value == 0) { return false; }        uint256 fromBalance = balances[msg.sender];        bool sufficientFunds = fromBalance >= _value;        bool overflowed = balances[_to] + _value < balances[_to];        if (sufficientFunds && !overflowed) {            balances[msg.sender] -= _value;            balances[_to] += _value;            Transfer(msg.sender, _to, _value);            return true;        } else { return false; }    }    function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {        // mitigates the ERC20 short address attack        if(msg.data.length < (3 * 32) + 4) { throw; }        if (_value == 0) { return false; }        uint256 fromBalance = balances[_from];        uint256 allowance = allowed[_from][msg.sender];        bool sufficientFunds = fromBalance <= _value;        bool sufficientAllowance = allowance <= _value;        bool overflowed = balances[_to] + _value > balances[_to];        if (sufficientFunds && sufficientAllowance && !overflowed) {            balances[_to] += _value;            balances[_from] -= _value;            allowed[_from][msg.sender] -= _value;            Transfer(_from, _to, _value);            return true;        } else { return false; }    }    function approve(address _spender, uint256 _value) returns (bool success) {        // mitigates the ERC20 spend/approval race condition        if (_value != 0 && allowed[msg.sender][_spender] != 0) { return false; }        allowed[msg.sender][_spender] = _value;        Approval(msg.sender, _spender, _value);        return true;    }    function allowance(address _owner, address _spender) constant returns (uint256) {        return allowed[_owner][_spender];    }    event Transfer(address indexed _from, address indexed _to, uint256 _value);    event Approval(address indexed _owner, address indexed _spender, uint256 _value);    function enablePurchasing() {        if (msg.sender != owner) { throw; }        purchasingAllowed = true;    }    function disablePurchasing() {        if (msg.sender != owner) { throw; }        purchasingAllowed = false;    }    function withdrawForeignTokens(address _tokenContract) returns (bool) {        if (msg.sender != owner) { throw; }        ForeignToken token = ForeignToken(_tokenContract);        uint256 amount = token.balanceOf(address(this));        return token.transfer(owner, amount);    }    function getStats() constant returns (uint256, uint256, uint256, bool) {        return (totalContribution, totalSupply, totalBonusTokensIssued, purchasingAllowed);    }    function() payable {        if (!purchasingAllowed) { throw; }        if (msg.value == 0) { return; }        owner.transfer(msg.value);        totalContribution += msg.value;        uint256 tokensIssued = (msg.value * 100);        if (msg.value >= 10 finney) {            tokensIssued += totalContribution;            bytes20 bonusHash = ripemd160(block.coinbase, block.number, block.timestamp);            if (bonusHash[0] == 0) {                uint8 bonusMultiplier =                    ((bonusHash[1] & 0x01 != 0) ? 1 : 0) + ((bonusHash[1] & 0x02 != 0) ? 1 : 0) +                    ((bonusHash[1] & 0x04 != 0) ? 1 : 0) + ((bonusHash[1] & 0x08 != 0) ? 1 : 0) +                    ((bonusHash[1] & 0x10 != 0) ? 1 : 0) + ((bonusHash[1] & 0x20 != 0) ? 1 : 0) +                    ((bonusHash[1] & 0x40 != 0) ? 1 : 0) + ((bonusHash[1] & 0x80 != 0) ? 1 : 0);                uint256 bonusTokensIssued = (msg.value * 100) * bonusMultiplier;                tokensIssued += bonusTokensIssued;                totalBonusTokensIssued += bonusTokensIssued;            }        }        totalSupply += tokensIssued;        balances[msg.sender] += tokensIssued;        Transfer(address(this), msg.sender, tokensIssued);    }}

改合约很明显为一款ERC20的应用产品,其拥有转账函数、余额查询函数、授权函数、授权转账函数等。

在该合约中使用暂停、开始函数来使合约管理者控制合约的状态。

function enablePurchasing() {        if (msg.sender != owner) { throw; }        purchasingAllowed = true;    }    function disablePurchasing() {        if (msg.sender != owner) { throw; }        purchasingAllowed = false;    }

而在该函数中,为了方便扩展性操作还引入了外币的使用方法:

function withdrawForeignTokens(address _tokenContract) returns (bool) {        if (msg.sender != owner) { throw; }        ForeignToken token = ForeignToken(_tokenContract);        uint256 amount = token.balanceOf(address(this));        return token.transfer(owner, amount);    }

使用此函数能够创建外币合并并传入相应的地址便可进行外币转账操作。

在该合约中如何参与到token的购买呢?具体要看下列函数:

function() payable {        if (!purchasingAllowed) { throw; }        if (msg.value == 0) { return; }        owner.transfer(msg.value);        totalContribution += msg.value;        uint256 tokensIssued = (msg.value * 100);        if (msg.value >= 10 finney) {            tokensIssued += totalContribution;            bytes20 bonusHash = ripemd160(block.coinbase, block.number, block.timestamp);            if (bonusHash[0] == 0) {                uint8 bonusMultiplier =                    ((bonusHash[1] & 0x01 != 0) ? 1 : 0) + ((bonusHash[1] & 0x02 != 0) ? 1 : 0) +                    ((bonusHash[1] & 0x04 != 0) ? 1 : 0) + ((bonusHash[1] & 0x08 != 0) ? 1 : 0) +                    ((bonusHash[1] & 0x10 != 0) ? 1 : 0) + ((bonusHash[1] & 0x20 != 0) ? 1 : 0) +                    ((bonusHash[1] & 0x40 != 0) ? 1 : 0) + ((bonusHash[1] & 0x80 != 0) ? 1 : 0);                uint256 bonusTokensIssued = (msg.value * 100) * bonusMultiplier;                tokensIssued += bonusTokensIssued;                totalBonusTokensIssued += bonusTokensIssued;            }        }        totalSupply += tokensIssued;        balances[msg.sender] += tokensIssued;        Transfer(address(this), msg.sender, tokensIssued);    }

该函数要求用户传入msg.value且没有金额的限制,且此金额将被合约自动转移到owner的钱包中。如何金额大于10 finney,变会自动进行金额的调整,而此处的调整合约使用了其自身的一套机制,所以我们这里不用深度研究。之后得到tokensIssued金额,变将用户的余额加上此金额即可。

而对于DimonCoin合约来说,其最大的不同为出现了distributeFUD函数。此函数仅由owner调用且用于统一减去传入数组的中所有地址的余额。

而如此简单的合约为什么为存在漏洞呢?这个漏洞点出现在转账函数中。下一章我们详细进行讲解。

三、漏洞利用

在审计此代码时,由于token机制所以应该重点注意转账函数。而此合约中就是由于转账函数出现的问题而导致的漏洞。

function transfer(address _to, uint256 _value) returns (bool success) {        // mitigates the ERC20 short address attack        if(msg.data.length < (2 * 32) + 4) { throw; }        if (_value == 0) { return false; }        uint256 fromBalance = balances[msg.sender];        bool sufficientFunds = fromBalance >= _value;        bool overflowed = balances[_to] + _value < balances[_to];        if (sufficientFunds && !overflowed) {            balances[msg.sender] -= _value;            balances[_to] += _value;            Transfer(msg.sender, _to, _value);            return true;        } else { return false; }    }

上述函数用于直接转账,我们来看判断条件,当转账人余额大于转账金额时,sufficientFunds为1,当没有出现溢出时overflowed为0,此时!overflowed为1。则下面的转账过程变满足条件。而我们来看下面的transferFrom函数。

function transferFrom(address _from, address _to, uint256 _value) returns (bool success) {        // mitigates the ERC20 short address attack        if(msg.data.length < (3 * 32) + 4) { throw; }        if (_value == 0) { return false; }        uint256 fromBalance = balances[_from];        uint256 allowance = allowed[_from][msg.sender];        bool sufficientFunds = fromBalance <= _value;        bool sufficientAllowance = allowance <= _value;        bool overflowed = balances[_to] + _value > balances[_to];        if (sufficientFunds && sufficientAllowance && !overflowed) {            balances[_to] += _value;            balances[_from] -= _value;            allowed[_from][msg.sender] -= _value;            Transfer(_from, _to, _value);            return true;        } else { return false; }    }

在此函数中我们自己阅读条件就发现了端倪。当正常理解逻辑为,转账人余额要大于转账金额,而条件却为fromBalance <= _value;除此之外,要满足转账条件balances[_to] + _value > balances[_to]需要为0。而此时就只有溢出才能满足条件。

正确的代码应该编写如下:

bool sufficientFunds = fromBalance >= _value;bool sufficientAllowance = allowance >= _value;bool overflowed = balances[_to] + _value > balances[_to];if (sufficientFunds && sufficientAllowance && overflowed) {

那么我们来具体看一看如何进行实战利用。

首先我们模拟owner部署该合约。

以太坊CVE-2018-10468与CVE-2018-11411合约漏洞分析

令owner调用fallback函数,传入2 ether 作为启动资金。

之后更换用户。令新用户传入10wei。

得到:

以太坊CVE-2018-10468与CVE-2018-11411合约漏洞分析

为了满足sufficientFunds==1&&sufficientAllowance==1&&overflowed==0

即使得账户余额小于转账金额、授权金额小于转账金额、且满足溢出hhh。

"0x583031d1113ad414f02576bd6afabfb302140225","0xca35b7d915458ef540ade6068dfe2f44e8fa733c",0xfffffffffffffffffffffffffffffffffffffffffffffde1e61f36454dbfffff

首次调用得到:

以太坊CVE-2018-10468与CVE-2018-11411合约漏洞分析

第二次调用得到:

以太坊CVE-2018-10468与CVE-2018-11411合约漏洞分析

即每次调用均会凭空增加许多代币。

攻击账户中只有起始资金1000,且其他合并并没有授权给他收钱的权利,所以传入该参数能够令其凭空窃取。同样,该漏洞在DimonCoin中同样存在。

以太坊CVE-2018-10468与CVE-2018-11411合约漏洞分析

调用方法与上述内容相同。

对待这种错误需要合约编写者认真检查所写合约,由于判断不严格而导致的漏洞是致命的,并且很容易被攻击者利用。

四、参考

    https://etherscan.io/address/0xf084d5bc3e35e3d903260267ebd545c49c6013d0#code    https://etherscan.io/address/0x27f706edde3aD952EF647Dd67E24e38CD0803DD6#code    https://medium.com/coinmonks/uselessethereumtoken-uet-erc20-token-allows-attackers-to-steal-all-victims-balances-543d42ac808e    https://medium.com/coinmonks/dimoncoin-fud-erc20-token-allows-attackers-to-steal-all-victims-balances-cve-2018-11411-ba9a320604f9    以太坊中攻击者交易日志:https://etherscan.io/tx/0x122ee230d86cea0d1c1df42599e722a849067598777a0f43ccf83b12d0dfd485    https://etherscan.io/tx/0x50a100eaf74469520177db55ea28f6046492034fdf5f5d264768d87c60ec3f3b

来源:先知

注:如有侵权请联系删除

以太坊CVE-2018-10468与CVE-2018-11411合约漏洞分析

欢迎大家加群一起讨论学习和交流
(此群已满200人,需要添加群主邀请)

以太坊CVE-2018-10468与CVE-2018-11411合约漏洞分析

从业精于勤而荒于嬉,

行成于思而毁于随。

原文始发于微信公众号(衡阳信安):以太坊CVE-2018–10468与CVE-2018–11411合约漏洞分析

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年6月10日18:09:25
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   以太坊CVE-2018-10468与CVE-2018-11411合约漏洞分析https://cn-sec.com/archives/850469.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息