先打个生活中的比方:
想象你家有个智能保险箱,正常得输密码才能开。可这保险箱有个设计漏洞,任何人按一下“重置键”,就能把自己设成主人,然后光明正大地把里面的金条拿走。
未授权访问攻击(Unauthorized Access Attack)就类似这样:黑客利用智能合约权限控制的漏洞,绕过身份验证,篡改状态或偷走资产。
未授权访问攻击是指攻击者利用合约中访问控制的缺陷,非法调用受限函数或修改关键数据。在以太坊上,这种攻击往往是因为开发者没给关键函数加权限限制,或者权限逻辑有漏洞,让外人能冒充合法用户。
2017年7月的Parity多重签名钱包事件就是个经典案例,今天我们就来拆解这场“血案”,看看它咋发生的,又该咋防。
Parity是以太坊生态里知名的钱包项目,主打多重签名(MultiSig)功能,让多人共同管理资产,号称更安全。2017年7月,他们推出1.5版本,升级了多签钱包的后台库(WalletLibrary),宣称优化了性能和安全性。可惜,这“升级”没堵住漏洞,反而留下了后门。
2017年7月19日,黑客盯上了Parity钱包,几个小时内偷走了15.3万ETH。具体过程是这样的:
第一步:冒充主人
黑客利用钱包合约的回退函数,通过delegatecall调用了库合约的initWallet函数。这个函数内部调用initMultiowned,把黑客地址(0xb3764761e297d6f121e79c32a65829cd1ddb4d32)设为owners,成功篡改了权限。
改权限现场:
https://etherscan.io/tx/0x9dbf0326a03a2a3719c27be4fa69aacc9857fd231a8d9dcaede4bb083def75ec
第二步:扫走资产
拿到控制权后,黑客调用execute函数,分批把钱包里的ETH转到了自己的地址:0xb3764761e297d6f121e79c32a65829cd1ddb4d32。
分批转账记录:
https://etherscan.io/txs?a=0xb3764761e297d6f121e79c32a65829cd1ddb4d32&p=2
最终获得:
https://etherscan.io/address/0xb3764761e297d6f121e79c32a65829cd1ddb4d32#internaltx
这次攻击卷走153,037 ETH(按当时ETH均价约200美元估算,约3060万美元),坑了不少用户和ICO项目。Parity紧急发布补丁,但被偷的钱已经拿不回来。社区一片哗然,大家开始质疑多签钱包的安全性,Parity的口碑也受了重创。
想看原版代码?戳这儿:
关键片段:
1. 回退函数敞开大门这段代码遇到带数据的调用,就直接用delegatecall执行库合约的函数,没设权限检查。
function() payable {
if (msg.value > 0)
Deposit(msg.sender, msg.value);
else if (msg.data.length > 0)
_walletLibrary.delegatecall(msg.data);
}
2. initWallet不设防本该受保护的初始化函数,任何人都能调用,重设主人。
function initWallet(address[] _owners, uint256 _required, uint256 _daylimit) {
initMultiowned(_owners, _required);
initDaylimit(_daylimit);
}
3. execute成提款机只要你是owners之一,就能提取资产。
function execute(address _to, uint256 _value, bytes _data) external returns (bytes32) {
require(isOwner(msg.sender));
if (_to.call.value(_value)(_data)) {
// 转账成功,否则交易回滚
}
}
致命漏洞:
回退函数的delegatecall没检查调用者,initWallet没加权限控制,黑客轻松改掉owners,再用execute转账。
contract Wallet {
address public _walletLibrary;
constructor(address libraryAddress) {
_walletLibrary = libraryAddress;
}
function() external payable {
if (msg.data.length > 0) { // 带数据就能触发
_walletLibrary.delegatecall(msg.data); // 调用initWallet
}
}
}
contract WalletLibrary {
address[] public owners;
function initWallet(address[] _owners, uint256 _required) public {
initMultiowned(_owners, _required);
}
function initMultiowned(address[] _owners, uint256 _required) internal {
owners = _owners; // 黑客篡改权限
}
function execute(address _to, uint256 _value, bytes _data) external {
require(isOwner(msg.sender), "Not authorized");
_to.call.value(_value)(_data);
}
function isOwner(address _addr) internal view returns (bool) {
for (uint256 i = 0; i < owners.length; i++) {
if (owners[i] == _addr) return true;
}
return false;
}
}
第一步:冒充主人
msg.data,调用initWallet,把自己地址(0xb3764761...)塞进去。delegatecall,触发initWallet和initMultiowned,篡改owners。第二步:提款跑路
execute,输入自己的地址和ETH数量,把钱包里的资产转走。delegatecall未限制调用者,initWallet缺乏检查,导致黑客能随意篡改owners。知道漏洞在哪,我们得防住。以下是三招实用办法:
address owner;
function() external payable {
require(msg.sender == owner, "Only owner allowed"); // 检查权限
if (msg.data.length > 0) {
_walletLibrary.delegatecall(msg.data);
}
}
效果:只有合约主人能调用delegatecall,外人没法插手。
bool initialized;
function initWallet(address[] _owners, uint256 _required) public {
require(!initialized, "Already initialized"); // 仅能初始化一次
initialized = true;
owners = _owners;
}
效果:初始化一次就锁住,黑客没法再改。
直接用OpenZeppelin的Ownable或AccessControl,再加全面的代码审计,确保没漏洞。
未授权访问攻击本质上是“权限漏洞”埋下的隐患。Parity事件证明,只要delegatecall和initWallet没管好,黑客就能靠execute卷走3060万美元。智能合约的安全,归根结底靠权限控制。管好每扇门,才能挡住不速之客。
