2020 年 10 月 8 号,去中心化钱包 imToken 发布推文表示,用户报告称 31 万枚 DAI 被盗,这与 DeFi Saver Exchange 冲突有关。DeFi Saver 对此回应称,被盗资金仍旧安全,正在接触受害用户。截至目前,资金已全部归还受害用户。笔者在收到情报后,针对这次发布 31 万枚 DAI 被盗事件展开具体的分析。本文由专栏作者 慢雾科技 撰稿,不代表动区立场。相关报导:资安月报DeFi 诈骗、跑路频发!9 月安全事件共 33 起,危害程度评级 HIGH

本文目录

背景攻击过程分析分析思路验证完整的攻击流程如下最后思考Defi是洗钱天堂?加密犯罪若出圈到 DeFi,监管风险不容小觑为什么投资者不用担心,KuCoin 遭骇近 2 亿美元会让以太坊崩跌?Bitfinex骇客比特币再转移 75 亿!4 年来累积转出近 400 亿赃款,恐成抛售危机

背景

2020 年 10 月8号,去中心化钱包 imToken 发布推文表示,用户报告称 31 万枚 DAI 被盗,这与 DeFi Saver Exchange 冲突有关。

DeFi Saver 对此回应称,被盗资金仍旧安全,正在接触受害用户。

截至目前,资金已全部归还受害用户。

早在今年 6 月份 DEFI Saver 就表示该团队发现 DEFI Save 应用系列中自有交易平台的一个漏洞,此次 31 万枚 DAI 被盗也与此前的 SaverExchange 合约漏洞有关。笔者在收到情报后,针对这次发布 31 万枚 DAI 被盗事件展开具体的分析。

攻击过程分析

查看这笔攻击交易:

https//etherscanio/tx/0xcd9dad40b409897d05fa0e60ed4e58eb99876febf94bc97679b7f45837ea86b7

其中可以看到被盗用户 0xc0 直接转出 31 万枚 DAI 到攻击合约 0x5b。

我们可以使用 OKO 浏览器查看具体的交易细节:

https//okopalkeocom/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] == KYBERETHADDRESS) { require(msgvalue gt= amount msgvalue smaller than amount) } else { require(ERC20(orderAddresses[1])transferFrom(msgsender 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] != KYBERETHADDRESS) { ERC20(orderAddresses[1])approve(address(ERC20PROXY0X) amount) } (success tokens[0] ) = takeOrder(orderAddresses callData address(this)balance amount) // either it reverts or order doesnt exist anymore we reverts as it was explicitely asked for this exchange require(success ampamp tokens[0] gt 0 0x transaction failed) wrapper = address(exchangeAddress) } if (tokens[0] == 0) { (wrapper price) = getBestPrice(amount orderAddresses[1] orderAddresses[2] exchangeType) require(price gt minPrice 0xPrice gt minPrice Slippage hit) // handle 0x exchange if equal price try 0x to use less gas if (0xPrice gt= price) { if (orderAddresses[1] != KYBERETHADDRESS) { ERC20(orderAddresses[1])approve(address(ERC20PROXY0X) amount) } (success tokens[0] tokens[1]) = takeOrder(orderAddresses callData address(this)balance amount) // either it reverts or order doesnt exist anymore if (success ampamp tokens[0] gt 0) { wrapper = address(exchangeAddress) emit Swap(orderAddresses[1] orderAddresses[2] amount tokens[0] wrapper) } } if (tokens[1] gt 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 gt minPrice Slippage hit onchain price) if (orderAddresses[1] == KYBERETHADDRESS) { (tokens[0]) = ExchangeInterface(wrapper)swapEtherToTokenvalue(tokens[1])(tokens[1] orderAddresses[2] uint(1)) } else { ERC20(orderAddresses[1])transfer(wrapper tokens[1]) if (orderAddresses[2] == KYBERETHADDRESS) { 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 gt 0) { msgsendertransfer(address(this)balance) } // return if there is any tokens left if (orderAddresses[2] != KYBERETHADDRESS) { if (ERC20(orderAddresses[2])balanceOf(address(this)) gt 0) { ERC20(orderAddresses[2])transfer(msgsender ERC20(orderAddresses[2])balanceOf(address(this))) } } if (orderAddresses[1] != KYBERETHADDRESS) { if (ERC20(orderAddresses[1])balanceOf(address(this)) gt 0) { ERC20(orderAddresses[1])transfer(msgsender ERC20(orderAddresses[1])balanceOf(address(this))) } }}

1 在代码第 5 行可以看到先对 orderAddresses [1] 是否为 KYBERETHADDRESS 地址已确定,由 orderAddresses [1] 为 DAI 合约地址,因此将直接调用从函数将数量为 amount 的 DAI 转入本协议。

DeFi安全漏洞

2 接下来在代码第 1112 行,通过 takeFee 函数计算费用,最终计算结果都为 0 时,不做这里展开。

BitGet官方网站

3 由于攻击者攻击的 exchangeType 为 4,因此将走代码第 22 行 ifexchangeType == 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]callvalue(value)(data) uint tokensLeft = amount uint tokensReturned = 0 if (success){ // check how many tokens left from src if (addresses[1] == KYBERETHADDRESS) { tokensLeft = address(this)balance } else { tokensLeft = ERC20(addresses[1])balanceOf(address(this)) } // check how many tokens are returned if (addresses[2] == KYBERETHADDRESS) { TokenInterface(WETHADDRESS)withdraw(TokenInterface(WETHADDRESS)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 函数签名000000000000000000000000c001cd7a370524209626e28eca6abe6cfc09b0e50000000000000000000000005bb456cd09d85156e182d2c7797eb49a438401870000000000000000000000000000000000000000000041a522386d9b95c00000 //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//approvesh/网站自查授权情况,避免帐户资产被恶意转出。

相关报导

Defi是洗钱天堂?加密犯罪若出圈到 DeFi,监管风险不容小觑为什么投资者不用担心,KuCoin 遭骇近 2 亿美元会让以太坊崩跌?Bitfinex骇客比特币再转移 75 亿!4 年来累积转出近 400 亿赃款,恐成抛售危机

LINE 与 Messenger 不定期为大家服务

Tags DaiimToken窃盗

Curve 的稳定币 crvUSD 即将推出,稳定费和 PegKeeper 可能改善 Curve 收入不足的问题。(前情提要:Curve 将推出稳定币?创办人暗示:是超额抵押,只能说这么多了)(事件背景:Curve稳定币机制》一文了解 LLAMMA 具体是如何工作的?)去中心化稳定币赛道市场广阔,但...

L2双强之一的Arbitrum,在代币策略上可能已经错过最好的战略时机。前情提要:速观 Arbitrum 发展近况:全面找寻 Alpha 机会背景补充:Arbitrum热门生态》Camelot的Launchpad申购教学、如何参与Factor Dao公募? 有不少小伙伴们都说 Arbitrum 一直...