先打个生活中的比方:
想象你家有个智能保险箱,正常得输密码才能开。可这保险箱有个设计漏洞,任何人按一下“重置键”,就能把自己设成主人,然后光明正大地把里面的金条拿走。
未授权访问攻击(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万美元。智能合约的安全,归根结底靠权限控制。管好每扇门,才能挡住不速之客。
基于全文检索与主题相似度
说在前面 生活中,我们习惯了依赖时间来安排一切。闹钟提醒我们起床,高铁准点发车,考试到点交卷,股票交易精确到毫秒撮合…… 在 Web2 世界里,软件系统同样靠精准的时间戳来自动执行任务,比如定时扣款、会员到期、订单结算,这一切都建立在可信的时间基础上。但到了 Web3 世界里,时间居然能被“调表”!
0转账钓鱼攻击是个啥? 0转账钓鱼攻击(Zero Transfer Phishing Attack)是一种简单却阴险的骗术,虽然技术含量不高,但在币圈屡屡得手,甚至酿成过惊天大案(见下文)。 每次听说这种攻击得逞,连我都忍不住暗想:这黑客咋不是我呢? 先打个生活中的比方: 假设你和好友张三经常通过银
说在前面 玩过币的人都知道,ERC20是以太坊上一个常见的代币标准(支持钱包交易交互),里面有个类似开支票的功能——approve授权和transferFrom转账兑现。 如同现实生活中你给人开了一张支票,写上金额,对方拿着支票去银行就能把钱取走。你账户里的钱少了,他的账户里钱多了。 在智能合约里,