本文部分内容基于安比(SECBIT)实验室团队与吴玉会(轻信科技)的讨论
本文结论: ERC223, ERC827的部分实现代码引入了任意函数调用缺陷,可能会对使用这部分代码的合约带来安全漏洞。如果需要实现上述规范接口,请仔细检查实现代码。这种合约本身允许用户自定义call()
任意地址上任意函数的设计,十分危险。攻击者可以很容易地借用当前合约的身份来进行任何操作,比如盗取Token或者绕开权限检查等。
影响范围:截止目前检测到以太坊上部署的受影响的ERC20合约数量:146
最新更新:
- 火币网已经暂停了已经上线交易的相关问题Token[9][10]
- ATN团队已经修复漏洞[1]
CUSTOM_CALL 滥用事件回顾与分析
2018 年 6 月 20 日,AI Technology Network (ATN) 和慢雾团队披露了一起针对 ATN 智能合约的攻击事件,黑客于 2018 年 5 月 11 日利用 ATN Token 合约存在的漏洞,将自己地址设为 owner 并增发获利 1100 万 ATN。ATN 技术团队迅速发现问题、定位攻击方法并完成合约的升级修复 [1]。黑客利用了 ERC223 合约可传入自定义的接收调用函数与 ds-auth 权限校验等特征,在 ERC223 合约调用这个自定义函数时,合约调用自身函数从而造成内部权限控制失效。随后,百度安全的“隐形人真忙”也在先知安全大会上进行了“以太坊智能合约 call 注入攻击”的主题分享 [2]。这个漏洞源于一个较为常见的做法:在调用合约函数之后,可以再次调用一次另一个合约的任意函数,并且这个任意函数可以由合约调用发起者指定。但是 ATN 的合约漏洞恰恰暴露了这一常见做法非常危险的一面:合约调用者可能通过该功能绕开权限检查,或者以合约的身份发起对其它合约的攻击等等。
有安全隐患代码链接:
https://github.com/Dexaran/ERC223-token-standard/blob/16d350ec85d5b14b9dc857468c8e0eb4a10572d3/ERC223_Token.sol#L70
https://github.com/OpenZeppelin/openzeppelin-solidity/blob/master/contracts/token/ERC827/ERC827Token.sol
ATN 事件漏洞分析
ERC223 是由 Dexaran 于 2017 年 3 月 5 日提出的一个 Token 标准草案 [3],用于改进 ERC20,解决其无法处理发往合约自身 Token 的这一问题。ERC20 有两套代币转账机制,一套为直接调用transfer()
函数,另一套为调用 approve() + transferFrom()
先授权再转账。当转账对象为智能合约时,这种情况必须使用第二套方法,否则转往合约地址的 Token 将永远无法再次转出。
下面代码为 ERC223 草案中的一段* 正确示例*,调用 transfer()
函数时,合约判断目标地址 to 是否是合约,如果是合约,则调用目标合约的tokenFallback()
方法,从而实现合约对转入 Token 的处理。这段代码并没有 CUSTOM_CALL 滥用的问题。
// 提案中的正确示例代码
contract ERC223 {
function transfer(address to, uint value, bytes data) {
uint codeLength;
assembly {
codeLength := extcodesize(_to)
}
balances[msg.sender] = balances[msg.sender].sub(_value);
balances[_to] = balances[_to].add(_value);
if(codeLength>0) {
// Require proper transaction handling.
ERC223Receiver receiver = ERC223Receiver(_to);
receiver.tokenFallback(msg.sender, _value, _data);
}
}
}
ERC223 合约是 ERC20 合约的超集,目标为取代 ERC20 合约,成为新的 Token 合约标准。但提出以来至今一年多的时间仍未得到广泛接受,仅有少数项目采用了该提案。
下面是 ERC223 错误实现代码,ATN Token不幸地采用了这一段代码。用户被允许传入任意自定义的_custom_fallback
,从而任意调用目标_to
地址上的任意方法!
// 此代码有 CUSTOM_CALL 滥用问题
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