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 └─ ← ()
|