10.lockbox
2023-08-16 19:38:15 # 19.Paradigm CTF 2021

lockbox

分析

1.全局观

  • Lockbox.sol
    • Entrypoint:解题的入口方法
    • 其他的所有合约:每个合约是过关斩将的其中一关。他们是通过套娃的方式联系在一起的,不同一般的在一个合约中设置过关斩将。其通过每个关卡的solve()的modifier进行连接(内联汇编)

2.任务

成功调用将solved设置为true

1
2
3
4
function solve(bytes4 guess) public _ {
require(guess == bytes4(blockhash(block.number - 1)), "do you feel lucky?");
solved = true;
}

3.详细分析

这种过关斩将的题目,我们应该从大限制开始做,然后再做小限制,顺序很重要,虽然一般情况是从1开始按顺序做。

stage5

1
2
3
function solve() public _ {
require(msg.data.length < 256, "a little too long");
}

我们的calldata数据长度必须小于256字节:如下是我们能够书写的calldata范围

1
2
3
4
5
6
7
8
9
0xe0d20f73 // keccak256("solve(bytes4))
0000000000000000000000000000000000000000000000000000000000000000 // 0x00
0000000000000000000000000000000000000000000000000000000000000000 // 0x20
0000000000000000000000000000000000000000000000000000000000000000 // 0x40
0000000000000000000000000000000000000000000000000000000000000000 // 0x60
0000000000000000000000000000000000000000000000000000000000000000 // 0x80
0000000000000000000000000000000000000000000000000000000000000000 // 0xa0
0000000000000000000000000000000000000000000000000000000000000000 // 0xc0
000000000000000000000000000000000000000000000000000000 // 0xe0

Entrypoint

1
2
3
4
function solve(bytes4 guess) public _ {
require(guess == bytes4(blockhash(block.number - 1)), "do you feel lucky?");
solved = true;
}

这个很容易做到,只需要修改第一个参数即可,此时的calldata为:

1
2
3
4
5
6
7
8
9
0xe0d20f73
[ guess]00000000000000000000000000000000000000000000000000000000 // 0x00
0000000000000000000000000000000000000000000000000000000000000000 // 0x20
0000000000000000000000000000000000000000000000000000000000000000 // 0x40
0000000000000000000000000000000000000000000000000000000000000000 // 0x60
0000000000000000000000000000000000000000000000000000000000000000 // 0x80
0000000000000000000000000000000000000000000000000000000000000000 // 0xa0
0000000000000000000000000000000000000000000000000000000000000000 // 0xc0
000000000000000000000000000000000000000000000000000000 // 0xe0

stage1

1
require(ecrecover(keccak256("stage1"), v, r, s) == 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf, "who are you?");

我们需要拿到0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf的私钥来对stage1来签名,并且不要加上以太坊签名消息前缀\x19Ethereum Signed Message:\n32

网络搜索0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf可以得到其私钥:0x0000000000000000000000000000000000000000000000000000000000000001

1
2
3
4
5
6
7
8
9
10
11
from eth_account import Account
from web3 import Web3

messagehash = Web3.keccak(text="stage1")
print("message's hash",messagehash.hex())
privatekey ="0x0000000000000000000000000000000000000000000000000000000000000001"
signMessage = Account.signHash(message_hash=messagehash, private_key=privatekey)

print("r = ", Web3.to_hex(signMessage.r))
print("s = ", Web3.to_hex(signMessage.s))
print("v = ", Web3.to_hex(signMessage.v))

得到结果

1
2
3
r =  0x370df20998cc15afb44c2879a3c162c92e703fc4194527fb6ccf30532ca1dd3b
s = 0x35b3f2e2ff583fed98ff00813ddc7eb17a0ebfc282c011946e2ccbaa9cd3ee67
v = 0x1b

此时calldata为:

1
2
3
4
5
6
7
8
9
0xe0d20f73
[ guess]0000000000000000000000000000000000000000000000000000001b // 0x00
370df20998cc15afb44c2879a3c162c92e703fc4194527fb6ccf30532ca1dd3b // 0x20
35b3f2e2ff583fed98ff00813ddc7eb17a0ebfc282c011946e2ccbaa9cd3ee67 // 0x40
0000000000000000000000000000000000000000000000000000000000000000 // 0x60
0000000000000000000000000000000000000000000000000000000000000000 // 0x80
0000000000000000000000000000000000000000000000000000000000000000 // 0xa0
0000000000000000000000000000000000000000000000000000000000000000 // 0xc0
000000000000000000000000000000000000000000000000000000 // 0xe0

stage2

1
2
3
function solve(uint16 a, uint16 b) public _ {
require(a > 0 && b > 0 && a + b < a, "something doesn't add up");
}

两个uint16相加很容易溢出(版本0.4.24),我们将低位修改为ff1b,满足结果:ff1b+dd3b=dc56

1
2
3
4
5
6
7
8
9
0xe0d20f73
[ guess]0000000000000000000000000000000000000000000000000000ff1b // 0x00
370df20998cc15afb44c2879a3c162c92e703fc4194527fb6ccf30532ca1dd3b // 0x20
35b3f2e2ff583fed98ff00813ddc7eb17a0ebfc282c011946e2ccbaa9cd3ee67 // 0x40
0000000000000000000000000000000000000000000000000000000000000000 // 0x60
0000000000000000000000000000000000000000000000000000000000000000 // 0x80
0000000000000000000000000000000000000000000000000000000000000000 // 0xa0
0000000000000000000000000000000000000000000000000000000000000000 // 0xc0
000000000000000000000000000000000000000000000000000000 // 0xe0

stage3

1
2
3
4
5
6
7
8
9
10
11
function solve(uint idx, uint[4] memory keys, uint[4] memory lock) public _ {
require(keys[idx % 4] == lock[idx % 4], "key did not fit lock");

for (uint i = 0; i < keys.length - 1; i++) {
require(keys[i] < keys[i + 1], "out of order");
}

for (uint j = 0; j < keys.length; j++) {
require((keys[j] - lock[j]) % 2 == 0, "this is a bit odd");
}
}
  • keys[idx % 4] == lock[idx % 4]:我们的idx%4为1b%4=27%4=3,此时就需要keys[3] = lock[3],然而由于stage5的限制,我们无法操作lock[3]的数据,它只能是在得到calldata之后补0。因此我们得到的v是不对的,不应该是1b,应该是1c,这样的话,1c%4=28%4=0,这时候只需要keys[0] = lock[0],这是可以做到的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    0xe0d20f73
    [ guess]0000000000000000000000000000000000000000000000000000ff1b // 0x00
    370df20998cc15afb44c2879a3c162c92e703fc4194527fb6ccf30532ca1dd3b // 0x20 keys[0]
    35b3f2e2ff583fed98ff00813ddc7eb17a0ebfc282c011946e2ccbaa9cd3ee67 // 0x40 keys[1]
    0000000000000000000000000000000000000000000000000000000000000000 // 0x60 keys[2]
    0000000000000000000000000000000000000000000000000000000000000000 // 0x80 keys[3]
    0000000000000000000000000000000000000000000000000000000000000000 // 0xa0 lock[0]
    0000000000000000000000000000000000000000000000000000000000000000 // 0xc0 lock[1]
    000000000000000000000000000000000000000000000000000000 // 0xe0 lock[2]
    lock[3]
  • keys[i] < keys[i + 1]:这个数组必须是递增的,目前我们构造的calldata不符合,因为keys[0]大于keys[1]。说明我们的vrs生成的不符合条件,我们知道,用相同的私钥对相同的消息进行签名,可以生成不同的签名,得到的一个符合条件的签名为:

1
2
3
4
5
6
7
Message: stage1
Message Hash: b6619a2d9d36a2acecba8e9d99c8444477624a46561077a675900f4af2c42c95
Signature: {
v: 28,
r: '10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de55468',
s: '53f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e62769'
}

因此,我们得到了新的符合条件的calldata:满足keys[idx % 4] == lock[idx % 4]keys[i] < keys[i + 1](keys[j] - lock[j]) % 2 == 0

1
2
3
4
5
6
7
8
9
10
0xe0d20f73
[ guess]0000000000000000000000000000000000000000000000000000ff1c // 0x00
10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de55468 // 0x20 keys[0]
53f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e62769 // 0x40 keys[1]
53f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e6276a // 0x60 keys[2]
53f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e6276c // 0x80 keys[3]
10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de55468 // 0xa0 lock[0]
0000000000000000000000000000000000000000000000000000000000000001 // 0xc0 lock[1]
000000000000000000000000000000000000000000000000000000 // 0xe0 lock[2]
lock[3]

stage4

1
2
3
function solve(bytes32[6] choices, uint choice) public _ {
require(choices[choice % 6] == keccak256(abi.encodePacked("choose")), "wrong choice!");
}

choose的哈希值为:0xe201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca896。要求我们找出在choices中找出一个bytes32,让他等于choose的哈希值。因此,我们可以将这个哈希值放到choices中,然后choice%6的结果是选择到它。我们就把这个哈希值放到choice[3]的位置吧。之后choice % 6需要等于3,那么choice可以为3。这样就满足了一切条件了,构造出来的calldata如下:

1
2
3
4
5
6
7
8
9
10
0xe0d20f73
[ guess]0000000000000000000000000000000000000000000000000000ff1c // 0x00 choice[0]
10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de55468 // 0x20 keys[0] choice[1]
53f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e62769 // 0x40 keys[1] choice[2]
e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca896 // 0x60 keys[2] choice[3]
e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca898 // 0x80 keys[3] choice[4]
10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de55468 // 0xa0 lock[0] choice[5]
0000000000000000000000000000000000000000000000000000000000000003 // 0xc0 lock[1]
000000000000000000000000000000000000000000000000000000 // 0xe0 lock[2]
lock[3]

最终的

经过上面过关斩将,我们确定了最终的calldata:

1
2
3
4
5
6
7
8
9
10
11
12
bytes4 guess = bytes4(blockhash(block.number - 1));

bytes memory data = abi.encodePacked(
bytes4(0xe0d20f73), // keccak256("solve(bytes4))
guess, bytes28(0x0000000000000000000000000000000000000000000000000000ff1c),
bytes32(0x10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de55468),
bytes32(0x53f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e62769),
bytes32(0xe201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca896),
bytes32(0xe201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca898),
bytes32(0x10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de55468),
bytes32(0x0000000000000000000000000000000000000000000000000000000000000003)
);

解题

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

import "forge-std/Test.sol";
import "./helper_setupBytecode.sol";
import "./interface.sol";

contract attackTest is Test {

ISetup public level;
IEntrypoint public entrypoint;

function setUp() public {
// 初始化题目
level = ISetup(deploySetup());
vm.label(address(level), "level");
entrypoint = IEntrypoint(level.entrypoint());
vm.label(address(entrypoint), "entrypoint");

// 在foundry中,每次测试的结果都是一样的,为了方便看trace,我们定下标签
vm.label(address(0x044AB9df2D2779933d10dfaF082540c0955B0307), "stage5");
vm.label(address(0xcAF4fdfB21455c48cBf8586eb02E4c09B4CE9B37), "stage4");
vm.label(address(0x2492Df72782081982f6344c92BDa4cB8e60eaA3E), "stage3");
vm.label(address(0x7f0Fd12Ce1780616AAd60aeF535ad5F8353a49d1), "stage2");
vm.label(address(0x41C3c259514f88211c4CA2fd805A93F8F9A57504), "stage1");
}

function test_isComplete() public{
bytes4 guess = bytes4(blockhash(block.number - 1));

bytes memory data = abi.encodePacked(
bytes4(0xe0d20f73),
guess, bytes28(0x0000000000000000000000000000000000000000000000000000ff1c),
bytes32(0x10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de55468),
bytes32(0x53f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e62769),
bytes32(0xe201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca896),
bytes32(0xe201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca898),
bytes32(0x10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de55468),
bytes32(0x0000000000000000000000000000000000000000000000000000000000000003)
);

// 不能直接调用solve(), 因为这样就没有后面的calldata了,我们要发送原始的calldata
// 可以用ethersjs来发送,也可以在solidity中用内联汇编

uint size = data.length;
address entry = address(entrypoint);
assembly{
switch call(gas(), entry, 0, add(data,0x20), size, 0, 0)
case 0 {
returndatacopy(0x00,0x00,returndatasize())
revert(0, returndatasize())
}
}

// 查看是否完成题目
assertEq(level.isSolved(),true);

}

function deploySetup() public returns (address addr) {
bytes memory bytecode = BYTECODE;
assembly {
addr := create(0, add(bytecode, 0x20), mload(bytecode))
}
}
}

通过trace可以看出,程序是从stage1到stage5(因为Stage中modifier的逻辑),并且calldata在每次solve()中都复用

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
Traces:
[52925] attackTest::test_isComplete()
├─ [39019] entrypoint::e0d20f73(290decd90000000000000000000000000000000000000000000000000000ff1c10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de5546853f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e62769e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca896e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca89810d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de554680000000000000000000000000000000000000000000000000000000000000003)
│ ├─ [228] stage1::034899bc()
│ │ └─ ← 0xf4ab27cc00000000000000000000000000000000000000000000000000000000
│ ├─ [30357] stage1::f4ab27cc(290decd90000000000000000000000000000000000000000000000000000ff1c10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de5546853f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e62769e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca896e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca89810d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de554680000000000000000000000000000000000000000000000000000000000000003)
│ │ ├─ [3000] PRECOMPILE::ecrecover(0xb6619a2d9d36a2acecba8e9d99c8444477624a46561077a675900f4af2c42c95, 28, 7607220488960343807138708896118077596332808739342783944889964628376182674536 [7.607e75], 37976160237878000092020130231526712820886658396819410882605408940802787649385 [3.797e76])
│ │ │ └─ ← 0x7E5F4552091A69125d5DfCb7b8C2659029395Bdf
│ │ ├─ [228] stage2::034899bc()
│ │ │ └─ ← 0x07e13e4d00000000000000000000000000000000000000000000000000000000
│ │ ├─ [21336] stage2::07e13e4d(290decd90000000000000000000000000000000000000000000000000000ff1c10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de5546853f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e62769e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca896e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca89810d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de554680000000000000000000000000000000000000000000000000000000000000003)
│ │ │ ├─ [228] stage3::034899bc()
│ │ │ │ └─ ← 0x3f30497e00000000000000000000000000000000000000000000000000000000
│ │ │ ├─ [15756] stage3::3f30497e(290decd90000000000000000000000000000000000000000000000000000ff1c10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de5546853f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e62769e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca896e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca89810d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de554680000000000000000000000000000000000000000000000000000000000000003)
│ │ │ │ ├─ [228] stage4::034899bc()
│ │ │ │ │ └─ ← 0x3b0a729200000000000000000000000000000000000000000000000000000000
│ │ │ │ ├─ [8374] stage4::3b0a7292(290decd90000000000000000000000000000000000000000000000000000ff1c10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de5546853f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e62769e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca896e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca89810d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de554680000000000000000000000000000000000000000000000000000000000000003)
│ │ │ │ │ ├─ [228] stage5::034899bc()
│ │ │ │ │ │ └─ ← 0x890d690800000000000000000000000000000000000000000000000000000000
│ │ │ │ │ ├─ [2335] stage5::890d6908(290decd90000000000000000000000000000000000000000000000000000ff1c10d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de5546853f5beb75699a068c70adbf9d545de94ec2511fe56862363799f44a700e62769e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca896e201a979a73f6a2947c212ebbed36f5d85b35629db25dfd9441d562a1c6ca89810d188c245dadc6b749cc5dedc56093db37a555fb80cacbc386f899f0de554680000000000000000000000000000000000000000000000000000000000000003)
│ │ │ │ │ │ └─ ← ()
│ │ │ │ │ └─ ← ()
│ │ │ │ └─ ← ()
│ │ │ └─ ← ()
│ │ └─ ← ()
│ └─ ← ()
├─ [3197] level::64d98f6e() [staticcall]
│ ├─ [436] entrypoint::799320bb()
│ │ └─ ← 0x0000000000000000000000000000000000000000000000000000000000000001
│ └─ ← 0x0000000000000000000000000000000000000000000000000000000000000001
└─ ← ()