2021WMCTF 1+2=3 复现

0x01 前言

复现2021WMCTF 一道blockchain赛题—1+2=3

0x02 题目分析

基于chainflag的框架,通过选项拿到题目源码

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
73
74
75
76
77
78
79
80
81
82
83
84
85
pragma solidity 0.8.0;

contract Dumper {
constructor(bytes memory code) {
assembly {
return(add(code, 0x20), mload(code))
}
}
}

interface Storage {
function getNumber() external view returns (uint256);
}

contract Puzzle {
Storage public Storage1;
Storage public Storage2;
Storage public Storage3;

bool public solved;

function check(bytes memory code) private returns (bool) {
uint256 i = 0;
while (i < code.length) {
uint8 op = uint8(code[i]);
if (
op == 0x3B || // EXTCODECOPY
op == 0x3C || // EXTCODESIZE
op == 0x3F || // EXTCODEHASH
op == 0x54 || // SLOAD
op == 0x55 || // SSTORE
op == 0xF0 || // CREATE
op == 0xF1 || // CALL
op == 0xF2 || // CALLCODE
op == 0xF4 || // DELEGATECALL
op == 0xF5 || // CREATE2
op == 0xFA || // STATICCALL
op == 0xFF // SELFDESTRUCT
) return false;

i++;
}

return true;
}

function reverse(bytes memory a) private returns (bytes memory) {
bytes memory b = new bytes(a.length);
for (uint256 i = 0; i < a.length; i++) {
b[b.length - i - 1] = a[i];
}
return b;
}

function sum(bytes memory a, bytes memory b) private returns (bytes memory) {
bytes memory c = new bytes(a.length);
for (uint256 i = 0; i < a.length; i++) {
uint8 q = uint8(a[i]) + uint8(b[i]);
c[i] = bytes1(q);
}
return c;
}

function deploy(bytes memory code) private returns (Storage) {
require(code.length <= 100);
require(check(code));

return Storage(address(new Dumper(code)));
}

function giveMeFlag(bytes memory code) public {
Storage1 = deploy(code);
require(Storage1.getNumber() == 1);
Storage2 = deploy(reverse(code));
require(Storage2.getNumber() == 2);
Storage3 = deploy(sum(code, reverse(code)));
require(Storage3.getNumber() == 3);

solved = true;
}

function isSolved() public view returns(bool) {
return solved;
}
}

分析giveMeFlag函数,需要满足三个require条件,进一步分析deploy函数,需要满足传入的code.length小于100,同时需要check验证:

1
2
3
4
5
6
7
8
9
10
11
12
0x3B -> EXTCODECOPY
0x3C -> EXTCODESIZE
0x3F -> EXTCODEHASH
0x54 -> SLOAD
0x55 -> SSTORE
0xF0 -> CREATE
0xF1 -> CALL
0xF2 -> CALLCODE
0xF4 -> DELEGATECALL
0xF5 -> CREATE2
0xFA -> STATICCALL
0xFF -> SELFDESTRUCT

0x03 POC构造

先看第一个require,要求Storage1.getNumber() == 1,我们可以看到interface Storage合约中没有完整实现getNumber() 函数,也就是说我们的字节码执行之后需要返回1。参考EthernautMagicNumber挑战,构造出字节码

1
600160005260206000f3

此时对应的反汇编

1
2
3
4
5
6
contract Contract {
function main() {
memory[0x00:0x20] = 0x01;
return memory[0x00:0x20];
}
}

在看第二个require,要求Storage1.getNumber() == 2。但是在此之前传入的code需要进行reverse,分析函数发现,其实现的是对code反转。那我们先构造出

1
2
3
4
5
6
7
600160005260206000f3
#按照字节反转
f3006020605200600260
#总字节码
600160005260206000f3f3006020605200600260
#reverse
600260005260206000f3f3006020605200600160

继续看第三个require,要求Storage1.getNumber() == 3。但是在此之前会执行将正序和逆序的字节码相加的字节码。按照我们之前的构造的显然无法满足。我们肯定是需要执行这一段字节码的

1
600360005260206000f3

也就是说对于上面的字节码我们需要填充一部分00来保证这一段字节码不被干扰,大体就是A+0+C+B,反转之后就是B+C+0+A,相加之后是D+C+C+D(D=A+B),现在的思路应该是如何在D+C+C+D这里面执行C,而不被D所干扰。我们根据52指令的解释

image-20210908151021428

其取的是栈顶的两个元素,如果我们将D先放入栈中,那就不会对我们的C执行造成干扰。去找PUSHx指令,根据D的长度,至少要选择PUSH10。接下去就是怎么在首尾出PUSH指令。这两个指令在require1和require2时会被首先执行,那些依赖于栈顶元素执行的指令不能选择,否则会失败。结果排查发现只能选择0x30左右的指令。此时我们先选择构造PUSH10,得到字节码如下

1
2
3
33600160005260206000f300000000000000000000f3006020605200600360f300602060520060026036
#reverse
36600260005260206000f3600360005260206000f300000000000000000000f300602060520060016033

我们可以很明显的看到在执行相加的时候,有两处f3相加,但是由于solidity 0.8.0 之后有算术溢出检测,如此会导致revert,所以我们需要错开两个f3,这就需要扩展A,B,多加一个字节。那我们就需要构造PUSH11。要保证不溢出,能选择的字节码空间很小。同时于是我选择在第一个f3之后加一个03,同时需要重新PUSH1 00,得到新的字节码

1
2
3
4
5
32600160005260206000f3006000600360005260206000f300000000000000000000f300600320600060520060026038
#revert
38600260005260006020036000f300000000000000000000f3006020605200600360006000f300602060520060016032
#sum
6ac003c000a4c020c020f66060f3600360005260206000f3f3006020605200600360f36060f620c020c0a400c003c06a

image-20210908162600147

0x04 思考

poc可以修改的地方:

  • 首尾字节码

  • 填充的03(sub),也可以用其他的运算符,只要最后在栈顶的是00,20就可以用f3返回

  • 开辟空间的位置也可以选择

1
2
3
4
5
6
7
600160205260206020f3
contract Contract {
function main() {
memory[0x20:0x40] = 0x01;
return memory[0x20:0x40];
}
}

简单的收获:

更加熟悉了以太坊虚拟机的字节码,相信对后面的智能合约逆向会有所帮助