今天看到了一则快讯蛮有意思
以太坊再现漏洞或使Token供应量增发
近日,黑客利用了一个ERC223合约与DS-AUTH库的混合漏洞,重设了owner权限,进行了ATN Token 的增发。ATN技术人员收到异常监控警示,介入后确定TOKEN合约受到黑客攻击并发现了相关漏洞,随后对漏洞进行了修复,并冻结了增发的Token。
另据慢雾区透露,ATN基金会将销毁1100万个ATN,并恢复ATN总量,同时将在主链上线映射时对黑客地址内的资产予以剔除,确保原固定总量不变。
这个漏洞蛮有意思哇!于是我搜了下原文想看看到底怎么回事,没想到是篇通稿= =
https://medium.com/@atnio/erc223-smart-contract-breach-and-resolution-vulnerability-relating-to-the-concurrent-9a402495f382
原文是英文的,除了大致介绍了漏洞的关键词,就在吹自己面对黑客的攻击如何临危不惧,如何跟著名的安全公司合作解决问题,帮助升级了不安全的ERC223标准。值得技术人员的留意的地方只有:
- 黑客通过一些手段窃取了ATN代币合约的控制权限
- 黑客使用控制权限增发了代币
- 黑客返还了控制权限,当作什么事情都没有发生
- 技术人员发现后暗不吭声,直到黑客想把增发的代币充值到交易所交易的时候立刻封锁了黑客的账户
这个“大致介绍”和“一些手段”真是耐人寻味……于是,轮到名侦探辰分出场啦!
黑客的攻击手段
原文里提供了个证明黑客想把增发的代币充值交易所的交易hash证明https://etherscan.io/tx/0x18bd80b810f6a6b6d397901d677657d39f8471069bcb7cfbf490c1946dfd617d ,多亏了这条链接,我准确定位到了黑客的攻击源地址是0x2eca25e9e19b31633db106341a1ba78accba7d0f
,在时间点May-11-2018 03:45:03 PM +UTC,这哥们实施了如下的交易攻击代码:
Function: transferFrom(address _from, address _to, uint256 _amount, bytes _data, string _custom_fallback)
MethodID: 0x4e2ab933
[0]: 0000000000000000000000002eca25e9e19b31633db106341a1ba78accba7d0f
[1]: 000000000000000000000000461733c17b0755ca5649b6db08b3e213fcf22546
[2]: 0000000000000000000000000000000000000000000000000000000000000000
[3]: 00000000000000000000000000000000000000000000000000000000000000a0
[4]: 00000000000000000000000000000000000000000000000000000000000000e0
[5]: 0000000000000000000000000000000000000000000000000000000000000000
[6]: 0000000000000000000000000000000000000000000000000000000000000000
[7]: 0000000000000000000000000000000000000000000000000000000000000011
[8]: 7365744f776e6572286164647265737329000000000000000000000000000000
如果对我之前的博文有所关注区块链智能合约:EVM封装bytes _extraData的一个BUG,那么看到这个就很容易得知黑客是通过封装一个看似合法的交易(transferFrom)对ATN合约进行权限改写的。我尝试把这个攻击交易复原成代码,得到了:
transferFrom("0x2eca25e9e19b31633db106341a1ba78accba7d0f",
"0x461733c17b0755ca5649b6db08b3e213fcf22546",
0,
["0x0"],
"setOwner(address)")
看到setOwner这种危险词,是不是就开始感觉不妙了?黑客就是故意越权引发了setOwner(address)函数,把代币增发权拿到了自己手里!
攻击代码的执行流程
我们稍微把攻击代码翻译一下成为人的自然语言:请从帐号"0x2eca25e9e19b31633db106341a1ba78accba7d0f"转帐0个ATN代币到地址"0x461733c17b0755ca5649b6db08b3e213fcf22546",附加信息是 ["0x0"],需要被执行的额外函数是"setOwner(address)"
注:"0x2eca25e9e19b31633db106341a1ba78accba7d0f"是黑客的以太坊地址,"0x461733c17b0755ca5649b6db08b3e213fcf22546"正是ATN的代币合约地址。
黑客在发送了攻击交易后,下面的代码就会被激活:
/*
* ERC 223
* Added support for the ERC 223 "tokenFallback" method in a "transfer" function with a payload.
*/
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;
}
可以看到这个函数的代码也就三部分,第一部分是一个权限控制,可以通过额外设定TokenController冻结某些帐号,在这里阻止被封的帐号移动资金; 第二部分就是require()那行,执行真正的转帐操作;第三部分则是提供附加函数支持的。
我还是通过上面的攻击代码解说具体流程,由于攻击交易只要求转帐0个ATN,所以交易在前三分之二都很合法,到了后三分之一部分,该代码首先把"0x461733c17b0755ca5649b6db08b3e213fcf22546"变成了一个可操作的ERC223ReceivingContract对象,这个对象其实在ATN代币合约里就有所定义:
/// @title ERC223ReceivingContract - Standard contract implementation for compatibility with ERC223 tokens.
contract ERC223ReceivingContract {
/// @dev Function that is called when a user or another contract wants to transfer funds.
/// @param _from Transaction initiator, analogue of msg.sender
/// @param _value Number of tokens to transfer.
/// @param _data Data containig a function signature and/or parameters
function tokenFallback(address _from, uint256 _value, bytes _data) public;
/// @dev For ERC20 backward compatibility, same with above tokenFallback but without data.
/// The function execution could fail, but do not influence the token transfer.
/// @param _from Transaction initiator, analogue of msg.sender
/// @param _value Number of tokens to transfer.
// function tokenFallback(address _from, uint256 _value) public;
}
可以看到,tokenFallback是个示例,真正的响应代码需要程序员后续添加编写,所以transferFrom里用了custom_fallback的形式,提供了后续开发最大的自由度:只要交易的发起人能准确指定自己想用"0x461733c17b0755ca5649b6db08b3e213fcf22546"中的哪个函数,那么这个函数就会被执行,黑客很聪明,直接调用了"setOwner(address)",把自己设定成"0x461733c17b0755ca5649b6db08b3e213fcf22546"的权限所有人。于是,他想干嘛就干嘛了(想增发就增发了)。
攻击代码暴露出的漏洞
- "0x461733c17b0755ca5649b6db08b3e213fcf22546"理应是一个ERC223对象实例,而不是一个ERC223ReceivingContract对象实例,黑客可以透过代码强行生成自己想要的对象实例,造成了不安全的基础(EVM和Solidity语言设计的漏洞)
- Solidity的开发指引明确说明,尽量不要用fallback来操作合约,前几年Multisig Wallet的漏洞频发正是有利的佐证,ATN开发人员为了后续开发的方便放开了fallback函数调用的口子,提供了不安全操作的通道(开发人员的代码编写问题)
- 黑客通过攻击交易,实际上生成了一个子交易操控了ATN合约的权限转移:
setOwner("0x2eca25e9e19b31633db106341a1ba78accba7d0f", 0, ["0x0"])
这个子交易的函数变量,明显与"setOwner(address)"的原始变量设定不同,但是它依然能运行成功,达到一样的效果,这是多!么!可!怕!的!事!情!EVM的设计真是一堆坑!!!!
mmmmmmmm……怪不得要语焉不详呢= =谁看到了这种漏洞还想用以太坊哟……
失效的攻击防御
实际上,ATN代币合约洋洋洒洒五六百行代码,是有针对"setOwner(address)"做执行的权限设定的:
function setOwner(address owner_) public auth {
owner = owner_;
LogSetOwner(owner);
}
auth正是权限设定的条件,我们来看看它具体是怎么写的:
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);
}
}
auth在发现交易发起人为owner或者是合约本身的情况下就会return true批准操作……由于黑客是通过fallback机制生成代币合约的子交易,发起人转变成了合约自己,所以这个安全闸门就被绕过了。
所以我一直认为多层权限控制是智能合约里需要避免的设计,虽然说对一个程序来说最危险的是用户(因为用户啥都不懂,容易乱操作引发bug);但最安全的也是用户,特别是你的管理员层级的用户(因为他们绝对没有捣乱的心,如果程序有不可预知的bug,可以通过用户的规范操作来避免bug的发生)。你把合约的控制交给另一个合约,还不如把控制权交给你信得过的人呢。
像ATN合约这种isAuthorized,还有现行的Multisig Wallet中的onlyWallet认证,都是我觉得不太安全的。EVM坑太多了,不要以身试坑啊!
ERC223,看起来很美?
上文所述的漏洞主要是跟智能合约的权限控制和fallback机制相关。但ERC223的设计初衷非常美好,是为了避免代币被不小心发到合约地址里无法提取,从而造成损失(https://github.com/ethereum/EIPs/issues/223)。
可是,可是,可是……避免这种情况,从ATN的解决方案看,并不需要有新的代币标准啊……
/// @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);
}
event ClaimedTokens(address indexed _token, address indexed _controller, uint _amount);
这就是一个预设的代币提取函数吧?跟新的代币标准有一毛钱的关系?
本-高贵的名侦探-辰分 陷入了深深的沉思……
本文谢绝无授权转载,转载请联系本人,谢谢!