動區動趨-最具影響力的區塊鏈新聞媒體
  • Home
    • Home Layout 1
    • Home Layout 2
    • Home Layout 3
  • Browse
    • News
    • Movie
    • Music
    • Technology
    • Howto & Style
    • Entertainment
    • Gaming
  • Features
    • Youtube Video
    • Vimeo Video
    • Dailymotion Video
    • Self-hosted Video
    • User Profile
    • Playlists
    • User-created Playlist
    • Favorite Playlist (Private)
    • Watch Later Playlist (Private)
    • All JNews Features
No Result
View All Result
  • Login
  • Register
UPLOAD
動區動趨-最具影響力的區塊鏈新聞媒體
No Result
View All Result
Currently Playing

ABS獨家專訪》Gitcoin共同創辦人Scott:台灣是現實與Web3治理的重要交匯點

ABS獨家專訪》Gitcoin共同創辦人Scott:台灣是現實與Web3治理的重要交匯點

ABS獨家專訪》Gitcoin共同創辦人Scott:台灣是現實與Web3治理的重要交匯點

搶先看
ABS獨家專訪》Gate.io CEO韓林:無懼銀行進軍加密服務,台北特別有人情味

ABS獨家專訪》Gate.io CEO韓林:無懼銀行進軍加密服務,台北特別有人情味

搶先看

6 Sci-fi Gadgets in Movie We Wish Actually Existed

Movie

The 10 best games to play on your new PlayStation 4

Gaming

Tesla’s Chinese factory just delivered its first cars

News

DeFi安全漏洞|解析:DeFi Saver用戶的「31萬枚DAI」是如何被盜的?

2020 年 10 月 8 號,去中心化錢包 imToken 發布推文表示,用戶報告稱 31 萬枚 DAI 被盜,這與 DeFi Saver Exchange 衝突有關。DeFi Saver 對此回應稱,被盜資金仍舊安全,正在接觸受害用戶。截至目前,資金已全部歸還受害用戶。筆者在收到情報後,針對這次發布 31 萬枚 DAI 被盜事件展開具體的分析。本文由專欄作者 慢霧科技 撰稿,不代表動區立場。
(相關報導:資安月報|DeFi 詐騙、跑路頻發!9 月安全事件共 33 起,危害程度評級 HIGH)

 

背景

2020 年 10 月8號,去中心化錢包 imToken 發布推文表示,用戶報告稱 31 萬枚 DAI 被盜,這與 DeFi Saver Exchange 衝突有關。

DeFi Saver 對此回應稱,被盜資金仍舊安全,正在接觸受害用戶。

⚠️ We received a user report of 310,000 DAI got drained which related to DeFi Saver Exchange vulnerability. Someone is front running the white hack. We suggest @DefiSaver suicide the contract to save people's money, immediately!https://t.co/OR9gA3hngi https://t.co/uJuLvJbSih

— imToken – Web3 Wallet (@imTokenOfficial) October 8, 2020

截至目前,資金已全部歸還受害用戶。

早在今年 6 月份 DEFI Saver 就表示該團隊發現 DEFI Save 應用系列中自有交易平台的一個漏洞,此次 31 萬枚 DAI 被盜也與此前的 SaverExchange 合約漏洞有關。筆者在收到情報後,針對這次發布 31 萬枚 DAI 被盜事件展開具體的分析。

攻擊過程分析

查看這筆攻擊交易:

https://etherscan.io/tx/0xcd9dad40b409897d05fa0e60ed4e58eb99876febf94bc97679b7f45837ea86b7

其中可以看到被盜用戶 0xc0 直接轉出 31 萬枚 DAI 到攻擊合約 0x5b。

我們可以使用 OKO 瀏覽器查看具體的交易細節:

https://oko.palkeo.com/0xcd9dad40b409897d05fa0e60ed4e58eb99876febf94bc97679b7f45837ea86b7

(以下不需閱讀代碼分析的讀者,可直接跳到總結整理的部分。)

從中可以插入攻擊者通過調用 swapTokenToToken 函數插入_exchangeAddress,_src,_dest 為 DAI 合約地址,選擇_exchangeType 為 4,並自定的 _callData。

進行具體的分析:(以下為詳細程式碼)

function swapTokenToToken(address _src, address _dest, uint _amount, uint _minPrice, uint _exchangeType, address _exchangeAddress, bytes memory _callData, uint _0xPrice) public payable {
    // use this to avoid stack too deep error
    address[3] memory orderAddresses = [_exchangeAddress, _src, _dest];

    if (orderAddresses[1] == KYBER_ETH_ADDRESS) {
        require(msg.value >= _amount, "msg.value smaller than amount");
    } else {
        require(ERC20(orderAddresses[1]).transferFrom(msg.sender, address(this), _amount), "Not able to withdraw wanted amount");
    }

    uint fee = takeFee(_amount, orderAddresses[1]);
    _amount = sub(_amount, fee);
    // [tokensReturned, tokensLeft]
    uint[2] memory tokens;
    address wrapper;
    uint price;
    bool success;

    // at the beggining tokensLeft equals _amount
    tokens[1] = _amount;

    if (_exchangeType == 4) {
        if (orderAddresses[1] != KYBER_ETH_ADDRESS) {
            ERC20(orderAddresses[1]).approve(address(ERC20_PROXY_0X), _amount);
        }

        (success, tokens[0], ) = takeOrder(orderAddresses, _callData, address(this).balance, _amount);
        // either it reverts or order doesn't exist anymore, we reverts as it was explicitely asked for this exchange
        require(success && tokens[0] > 0, "0x transaction failed");
        wrapper = address(_exchangeAddress);
    }

    if (tokens[0] == 0) {
        (wrapper, price) = getBestPrice(_amount, orderAddresses[1], orderAddresses[2], _exchangeType);

        require(price > _minPrice || _0xPrice > _minPrice, "Slippage hit");

        // handle 0x exchange, if equal price, try 0x to use less gas
        if (_0xPrice >= price) {
            if (orderAddresses[1] != KYBER_ETH_ADDRESS) {
                ERC20(orderAddresses[1]).approve(address(ERC20_PROXY_0X), _amount);
            }
            (success, tokens[0], tokens[1]) = takeOrder(orderAddresses, _callData, address(this).balance, _amount);
            // either it reverts or order doesn't exist anymore
            if (success && tokens[0] > 0) {
                wrapper = address(_exchangeAddress);
                emit Swap(orderAddresses[1], orderAddresses[2], _amount, tokens[0], wrapper);
            }
        }

        if (tokens[1] > 0) {
            // in case 0x swapped just some amount of tokens and returned everything else
            if (tokens[1] != _amount) {
                (wrapper, price) = getBestPrice(tokens[1], orderAddresses[1], orderAddresses[2], _exchangeType);
            }

            // in case 0x failed, price on other exchanges still needs to be higher than minPrice
            require(price > _minPrice, "Slippage hit onchain price");
            if (orderAddresses[1] == KYBER_ETH_ADDRESS) {
                (tokens[0],) = ExchangeInterface(wrapper).swapEtherToToken.value(tokens[1])(tokens[1], orderAddresses[2], uint(-1));
            } else {
                ERC20(orderAddresses[1]).transfer(wrapper, tokens[1]);

                if (orderAddresses[2] == KYBER_ETH_ADDRESS) {
                    tokens[0] = ExchangeInterface(wrapper).swapTokenToEther(orderAddresses[1], tokens[1], uint(-1));
                } else {
                    tokens[0] = ExchangeInterface(wrapper).swapTokenToToken(orderAddresses[1], orderAddresses[2], tokens[1]);
                }
            }

            emit Swap(orderAddresses[1], orderAddresses[2], _amount, tokens[0], wrapper);
        }
    }

    // return whatever is left in contract
    if (address(this).balance > 0) {
        msg.sender.transfer(address(this).balance);
    }

    // return if there is any tokens left
    if (orderAddresses[2] != KYBER_ETH_ADDRESS) {
        if (ERC20(orderAddresses[2]).balanceOf(address(this)) > 0) {
            ERC20(orderAddresses[2]).transfer(msg.sender, ERC20(orderAddresses[2]).balanceOf(address(this)));
        }
    }

    if (orderAddresses[1] != KYBER_ETH_ADDRESS) {
        if (ERC20(orderAddresses[1]).balanceOf(address(this)) > 0) {
            ERC20(orderAddresses[1]).transfer(msg.sender, ERC20(orderAddresses[1]).balanceOf(address(this)));
        }
    }
}

1. 在代碼第 5 行可以看到先對 orderAddresses [1] 是否為  KYBER_ETH_ADDRESS 地址已確定,由 orderAddresses [1] 為 DAI 合約地址,因此將直接調用從函數將數量為 _amount 的 DAI 轉入本協議。

2. 接下來在代碼第 11,12 行,通過 takeFee 函數計算費用,最終計算結果都為 0 時,不做這裡展開。

3. 由於攻擊者攻擊的 _exchangeType 為 4,因此將走代碼第 22 行 if(_exchangeType == 4)的邏輯。在代碼中我們可以裁剪在此邏輯中調用了 order 函數,並進行了攻擊者自定的 _callData,注意這將是本次攻擊的關鍵點,隨後切入 takeOrder 函數:

function takeOrder(address[3] memory _addresses, bytes memory _data, uint _value, uint _amount) private returns(bool, uint, uint) {
        bool success;

        (success, ) = _addresses[0].call.value(_value)(_data);

        uint tokensLeft = _amount;
        uint tokensReturned = 0;
        if (success){
            // check how many tokens left from _src
            if (_addresses[1] == KYBER_ETH_ADDRESS) {
                tokensLeft = address(this).balance;
            } else {
                tokensLeft = ERC20(_addresses[1]).balanceOf(address(this));
            }

            // check how many tokens are returned
            if (_addresses[2] == KYBER_ETH_ADDRESS) {
                TokenInterface(WETH_ADDRESS).withdraw(TokenInterface(WETH_ADDRESS).balanceOf(address(this)));
                tokensReturned = address(this).balance;
            } else {
                tokensReturned = ERC20(_addresses[2]).balanceOf(address(this));
            }
        }

        return (success, tokensReturned, tokensLeft);
    }

4. 在 takeOrder 函數中的第4行,我們可以直觀的修剪此邏輯可對目標 _addresses [0] 的函數進行調用,此時 _addresses [0] 為_exchangeAddress 即 DAI 合約地址,而具體的調用即攻擊者自定值的 _callData,因此如果持有 DAI 用戶在 DAI 合約中對 SaverExchange 合約進行過授權,則可以通過調用的 _callData 調用DAI 合約的 transfer 從函數將用戶的 DAI 直接轉出,具體都可以在_callData 中進行構造。

5. 接下來由於返回的令牌 [0] 為 1,所以將走 swapTokenToToken 函數代碼塊中第 76 行以下的邏輯,如果判斷的邏輯,可以看到都是使用,毫無疑問可以走通。

分析思路驗證

讓我們通過攻擊者的操作來驗證此過程是否如我們所想:

1. 通過鏈上記錄可以看到,被盜的用戶歷史上有對 SaverExchange合約進行 DAI 的授權,交易哈希如下:

0xdcf73848022ec1f730d9fdb90f4e8563f0dff48d9191aab19fc51241708eacf0

2. 通過鏈上數據可以發現預期的 _callData 為:

23b872dd //SlowMist// transferFrom 函数签名
000000000000000000000000c001cd7a
370524209626e28eca6abe6cfc09b0e5
0000000000000000000000005bb456cd
09d85156e182d2c7797eb49a43840187
00000000000000000000000000000000
00000000000041a522386d9b95c00000 //SlowMist// 310000e18

3. 通過鏈上調用過程可研磨攻擊者直接調用 DAI 合約的 transferFrom 函數將被盜用戶的 31 萬枚 DAI 轉走:

完整的攻擊流程如下

1. 攻擊者調用 swapTokenToToken 函數為 _exchangeAddress 為 DAI 合約地址,選擇 _exchangeType 為 4,將攻擊有效載荷置於_callData 中。

2. 此時將走 _exchangeType == 4 的邏輯,這將調用 takeOrder 函數並寫入 _callData。

3. takeOrder 函數將對預期的 _callData 進行具體調用,因此如果持有 DAI 用戶在 DAI 合約中對 SaverExchange 合約進行過授權,則可以通過的 _callData 調用 DAI 合約的 transfer 來自函數將用戶的 DAI直接轉出,具體都可以在 _callData 中進行構造。

4. 通過構造的 _callData 與相對用戶對 SaverExchange 合約進行過 DAI 的授權,SaverExchange 合約可以通過調用 DAI 合約的 transfer從函數將用戶帳戶中的 DAI 直接轉出至攻擊者指定的地址。

最後思考

此突破的關鍵在於攻擊者可以通過 takeOrder 函數對目標合約_addresses [0] 的任意函數進行任意調用,而引入 takeOrder 函數的參數都是用戶可控的,並且未對參數進行任何檢查或限制。

因此,為避免出現此類問題,建議項目方使用白名單策略對用戶調用的 _callData 等參數進行檢查,或者結合項目方具體的業務場景尋找更好的調用方式,而不是不做任何限制的進行隨意調用。

此漏洞不僅只影響到通過 DAI 合約對 SaverExchange 合約授權過的用戶,如果用戶歷史對 SaverExchange 合約有進行過其他令牌的授權,則都會存在帳戶的令牌被任意轉出風險。

建議此前有對 SaverExchange 合約進行過授權的用戶盡快取消授權(推薦使用https://approve.sh/網站自查授權情況),避免帳戶資產被惡意轉出。

📍相關報導📍

Defi是洗錢天堂?加密犯罪若出圈到 DeFi,監管風險不容小覷

為什麼投資者不用擔心,KuCoin 遭駭近 2 億美元會讓以太坊崩跌?

Bitfinex駭客比特幣再轉移 7.5 億!4 年來累積轉出近 400 億贓款,恐成拋售危機


讓動區 Telegram 新聞頻道再次強大!!立即加入獲得第一手區塊鏈、加密貨幣新聞報導。

LINE 與 Messenger 不定期為大家服務

加入好友

加入好友

No Result
View All Result

近期文章

  • 精選文章搶先看!動區登入Access質押訂閱服務,解鎖寶貴資訊快人一步
  • ABS獨家專訪》Gitcoin共同創辦人Scott:台灣是現實與Web3治理的重要交匯點
  • ABS獨家專訪》Gate.io CEO韓林:無懼銀行進軍加密服務,台北特別有人情味
  • 快訊!BTC 現在已來到 58996.2
  • 快訊!BTC 現在已來到 58815.03
Next Post
華爾街債券天王:我不信比特幣;全球股市18個月內極為艱難,美元將持續疲軟

華爾街債券天王:我不信比特幣;全球股市18個月內極為艱難,美元將持續疲軟

Copyright (c) 2019 by Jegtheme.
  • About
  • Buy JNews
  • Request A Demo
  • Contact

Welcome Back!

Login to your account below

Forgotten Password? Sign Up

Create New Account!

Fill the forms below to register

All fields are required. Log In

Retrieve your password

Please enter your username or email address to reset your password.

Log In

Add New Playlist

- Select Visibility -

    No Result
    View All Result
    • Account
    • BlockTempo Beginner – 動區新手村
    • Change Password
    • Forgot Password?
    • Home 1
    • Home 2
    • Home 3
    • Jin-homepage
    • Latest
    • Login
    • Profile
    • Register
    • Reset Password
    • Trending
    • Users
    • Users List Item
    • 不只加密貨幣,談談那些你不知道的區塊鏈應用|動區新手村
    • 所有文章
    • 關於 BlockTempo

    © 2025 JNews - Premium WordPress news & magazine theme by Jegtheme.