ETH Varible Overlap

经过对 ETH 以及其运行时环境 EVM 的初步研究,我们在合约层面和虚拟机层面分别发现了一些问题,其中有些问题可能导致非常严重的后果,值得 ETH 智能合约的开发者注意。变量覆盖问题就是其中非常典型的一种,有很多以太坊安全研究人员都已经发现了这个问题,但是分析大都停留在结果层面,没有做进一步的探讨。本文将从变量覆盖这个问题入手结合 EVM 虚拟机和 Solidity 编译器的源码详细分析这个问题的表现,可能产生的影响,以及最终导致这个问题的 solidity 编译器根源所在。

变量覆盖

在某些合约中,我们发现在函数内部对 struct 类型的临时变量进行修改,会在某些情况下覆盖已有的全局变量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pragma solidity ^0.4.23;
// A Locked Name Registrar
contract Locked {
bool public unlocked = false; // registrar locked, no name updates
struct NameRecord { // map hashes to addresses
bytes32 name; //
address mappedAddress;
}
mapping(address => NameRecord) public registeredNameRecord; // records who registered names
mapping(bytes32 => address) public resolve; // resolves hashes to addresses
function register(bytes32 _name, address _mappedAddress) public {
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
resolve[_name] = _mappedAddress;
registeredNameRecord[msg.sender] = newRecord;
require(unlocked); // only allow registrations if contract is unlocked
}
}

合约的源码如上面所示,在正常情况下,由于合约并没有提供修改 unlocked 的接口,因此不太可能达到修改它的目的。但是实际上我们在测试中发现,只要调用合约的 register 方法就可以修改 unlocked。

下面根据 bytecode 对合约的运行原理进行直观的分析。首先来看 unlocked,由于 unlocked 的属性 public 因此在合约遍历过程中会自动生成函数 getter。调试 getter,opcode 的作用写在对应语句的注释中

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
227 JUMPDEST
228 CALLVALUE ; 获取这次调用发送来的 value
229 DUP1 ; 开始操作 value,获取 value 的副本
230 ISZERO ; 判断是否有传入 value
231 PUSH2 00ef ;
234 JUMPI ; 如果 ISZERO 的结果是 1 ,即没有传入 value则跳转到 0xef 位置的 opcode
235 PUSH1 00 ;
237 DUP1 ;
238 REVERT ;
239 JUMPDEST ; 跳转到这里
240 POP ; 清理上次操作的痕迹,释放栈空间,即 value
241 PUSH2 00f8 ; 记录返回值
244 PUSH2 0246 ;
247 JUMP ; 跳转到 0x246 位置
248 JUMPDEST ;
249 PUSH1 40 ;
251 MLOAD ; 获取 Memory 中 0x40 位置的值 mem1
252 DUP1 ; mem1
253 DUP3 ; unlocked_data
254 ISZERO ;
255 ISZERO ;
256 ISZERO ;
257 ISZERO ; !!!!(unlocked_data)
258 DUP2 ; mem1
259 MSTORE ; 将值写到 mem1 所代表的位置上
260 PUSH1 20 ;
262 ADD ; mem1+0x20
263 SWAP2 ;
264 POP ;
265 POP ;
266 PUSH1 40 ;
268 MLOAD ; 获取 Memory 中 0x40 位置的值 mem1
269 DUP1 ; mem1
270 SWAP2 ;
271 SUB ; mem1+0x20 - mem1
272 SWAP1 ; mem1 作为返回值的地址
273 RETURN ;
......
582 JUMPDEST
583 PUSH1 00 ; unlocked 在 storage 中的位置 slot1
585 DUP1 ;
586 SWAP1 ;
587 SLOAD ; 加载 storage 中 slot1 位置的值 data1
588 SWAP1 ;
589 PUSH2 0100 ;
592 EXP ; 100^slot1
593 SWAP1 ;
594 DIV ; data1/(100^slot1)
595 PUSH1 ff ;
597 AND ; (data1/(100^slot1)) & 0xff EVM 是大端的,获取最高2位 unlocked_data
598 DUP2 ;
599 JUMP ; 返回 0xf8 位置

可以看到 unlocked 存储在 storage 中偏移为1 的位置。接着分析 register 方法,为了方便调试,我们只保留关键部分

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
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
600 JUMPDEST ; 函数起始位置,此时栈顶的数据依次是传入的两个参数 _mappedAddress、_name
601 PUSH1 00 ; newRecord 在 storage 中的位置 slot1:0
603 DUP3 ; _name
604 DUP2 ; slot1
605 PUSH1 00 ; NewRecord.name 的相对偏移 off1
607 ADD ; NewRecord.name 在 storage 中的位置 slot2:0
608 DUP2 ; _name
609 PUSH1 00 ;
611 NOT ;
612 AND ; _name = 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff & _name
613 SWAP1 ;
614 SSTORE ; storage[slot2] = _name
615 POP ; 清理痕迹 _name
616 DUP2 ; _mappedAddress
617 DUP2 ; slot1
618 PUSH1 01 ; NewRecord.mappedAddress 的相对偏移 off2
620 ADD ; NewRecord.mappedAddress 在 storage 中的位置 slot3:1
621 PUSH1 00 ;
623 PUSH2 0100 ;
626 EXP ; 100^0
627 DUP2 ; slot3
628 SLOAD ; 获取 NewRecord.mappedAddress 原值 data1
629 DUP2 ; 100^0
630 PUSH20 ffffffffffffffffffffffffffffffffffffffff ; 清空 data1 中后 32 位
651 MUL ;
652 NOT ; mask = 0xffffffffffffffffffffffff0000000000000000000000000000000000000000
653 AND ; data1 & mask
654 SWAP1 ; swap(data1 & mask, 100^0)
655 DUP4 ; _mappedAddress
656 PUSH20 ffffffffffffffffffffffffffffffffffffffff ; 限定 address 为后 32 位
677 AND ; ffffffffffffffffffffffffffffffffffffffff & _mappedAddress
678 MUL ; _mappedAddress * 100^0
679 OR ; (_mappedAddress * 100^0) & (data1 & mask)
680 SWAP1 ;
681 SSTORE ; storage[slot3] = (_mappedAddress * 100^0) & (data1 & mask)
682 POP ;
683 POP ;
684 POP ;
685 POP ;
686 JUMP ; 函数返回

可以看到 newRecord 对象在 storage 部分的索引位置也是 0 和全局 unlocked 相重叠,因此访问 newRecord 的时候也会顺便修改到 unlocked。

造成这一现象的原因我们猜想是编译器在处理局部变量和全局变量时均从 0 开始计算下标造成的。

为了验证这一猜想,我们在函数中再添加一个全局变量 unknow ,并同时添加一个函数 GetSome

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
contract Locked {
bool public unlocked = false; // registrar locked, no name updates
uint public unknow = 0x10adbeef;
struct NameRecord { // map hashes to addresses
bytes32 name; //
address mappedAddress;
}
struct Unknow { // map hashes to addresses
uint aha; //
uint ama;
}
function register(bytes32 _name, address _mappedAddress) public payable{
// set up the new NameRecord
NameRecord newRecord;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
}
function GetSome() returns (uint){
Unknow un;
return un.ama;
}
}

直接访问这个方法,返回 0x10adbeef;调用 register ,rgister('0x414141', 0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db), 再次访问 GetSome, 返回 0x4b0897b0513fdc7c541b6d9d7e929c4e5364d2db。

同样的,我们查看 GetSome 对应的 Opcode,可以看到访问的 slot 果然还是从 0 开始的。 看起来我们的猜想是正确的

1
2
3
4
5
6
7
8
9
10
11
305 PUSH1 00 ; un 在 storage 中的位置 slot1
307 DUP1 ; slot1
308 DUP1 ; slot1
309 PUSH1 01 ; ama 相对于 Unknow 的偏移 off1
311 ADD ;
312 SLOAD ; storage[slot1 + off1]
313 SWAP2 ;
314 POP ;
315 POP ;
316 SWAP1 ;
317 JUMP ;

继续修改,在 register 方法中添加 Unknow 结构体

1
2
3
4
5
6
7
8
9
function register(bytes32 _name, address _mappedAddress) public payable returns (uint){
// set up the new NameRecord
NameRecord newRecord;
Unknow un;
newRecord.name = _name;
newRecord.mappedAddress = _mappedAddress;
return un.ama;
}

访问这个 register 函数,我们发现情况和我们的猜想有出入,

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
288 JUMPDEST
289 PUSH1 00 ; newRecord 在 storage 中的位置 slot1
291 DUP1 ; slot1
292 PUSH1 00 ; un 在 storage 中的位置 slot2
294 DUP5 ; _name
295 DUP3 ; dup_slot1
296 PUSH1 00 ; NewRecord.name 的相对偏移 off1
298 ADD ;
299 DUP2 ; _name
300 PUSH1 00 ;
302 NOT ;
303 AND ;
304 SWAP1 ;
305 SSTORE ; storage[slot1+off1] = _name
306 POP ;
307 DUP4 ; _mappedAddress
308 DUP3 ; slot1
309 PUSH1 01 ; NewRecord.mappedAddress 的相对偏移 off2
311 ADD ;
312 PUSH1 00 ;
314 PUSH2 0100 ;
317 EXP ; 100^0
318 DUP2 ; slot1 + off2
319 SLOAD ;
320 DUP2 ;
321 PUSH20 ffffffffffffffffffffffffffffffffffffffff ;
342 MUL ;
343 NOT ;
344 AND ;
345 SWAP1 ;
346 DUP4 ;
347 PUSH20 ffffffffffffffffffffffffffffffffffffffff ;
368 AND ;
369 MUL ;
370 OR ;
371 SWAP1 ;
372 SSTORE ; storage[slot3] = (_mappedAddress * 100^0) & (data1 & mask)
373 POP ;
374 DUP1 ; slot2
375 PUSH1 01 ; ama 相对于 Unknow 的偏移 off3
377 ADD ;
378 SLOAD ; storage[slot2 + off3]
379 SWAP3 ;
380 POP ;
381 POP ;
382 POP ;
383 SWAP3 ;
384 SWAP2 ;
385 POP ;
386 POP ;
387 JUMP ;

很明显的在这个方法中 storage 对象和 un 对象所占用的 slot 是同一个,即所有的临时变量都是从 storage 的 0 位置开始存储的!!!!

如果我们多设置几个临时变量,会发现情况一致,在函数开始选定 slot 时,所有的临时变量对应的 slot 值都是 0!!!

继承

对于继承而言,情况是类似的,虽然全局变量的 slot 会从父合约开始,但是函数中临时变量的 slot 值仍然都是 0。对临时变量的修改仍然会影响包括子类和父类的所有全局对象

聚合

对于聚合而言,主合约中存储的实际上是其他合约的地址。 在主合约中的临时变量操作只能够重写这个合约地址,而无法影响到其他合约

map

我们继续修改代码,将 Unknow 的结构体变大,看看是否可以泄露出更多的信息。但是结果失败了,并没有泄露出后面的 Map 信息。

于是我们查看一下对于 storage 中 map 的操作,调试 opcode 接着 newRecord.mappedAddress = _mappedAddress; 后面的部分

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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
0627 SSTORE ; newRecord.mappedAddress = _mappedAddress;
0628 POP ;
0629 DUP2 ; _mappedAddress
0630 PUSH1 02 ; resolve 在 storage 中所处的位置 slot1 = 2
0632 PUSH1 00 ; resolve 所用的临时存储位置 mem1
0634 DUP6 ; _name
0635 PUSH1 00 ;
0637 NOT ;
0638 AND ;
0639 PUSH1 00 ;
0641 NOT ;
0642 AND ; 限定一下 _name
0643 DUP2 ; mem1
0644 MSTORE ; memory[mem1] = _name
0645 PUSH1 20 ;
0647 ADD ; 下一个存储位置 mem2 = mem1 + 0x20
0648 SWAP1 ;
0649 DUP2 ;
0650 MSTORE ; memory[mem2] = slot1
0651 PUSH1 20 ;
0653 ADD ; 下一个 memory 位置 mem3
0654 PUSH1 00 ; mem_start
0656 SHA3 ; 计算一下从 memstart ———— mem3 这段内存空间的 hash 值 sha1 = sha(_name,slot1)
0657 PUSH1 00 ;
0659 PUSH2 0100 ;
0662 EXP ; 100^0
0663 DUP2 ; sha1
0664 SLOAD ; sdata1 = storage[sha3] 获取指定位置的原数据
0665 DUP2 ; 100^0
0666 PUSH20 ffffffffffffffffffffffffffffffffffffffff ; 清空 sdata1 后 32位
0687 MUL ;
0688 NOT ; mask = 0xffffffffffffffffffffffff0000000000000000000000000000000000000000
0689 AND ; sdata1 & mask
0690 SWAP1 ;
0691 DUP4 ; _mappedAddress
0692 PUSH20 ffffffffffffffffffffffffffffffffffffffff ; 限定 address 为后 32 位
0713 AND ;
0714 MUL ;
0715 OR ; sdata1 & mask | _mappedAddress
0716 SWAP1 ;
0717 SSTORE ; storage[sha3] = sdata1 & mask | _mappedAddress
0718 POP ;
0719 DUP1 ;
0720 PUSH1 01 ; registeredNameRecord 在 storage 中所处的位置 slot2 = 1
0722 PUSH1 00 ; registeredNameRecord 所用的临时存储位置 mem1
0724 CALLER ; msg.sender
0725 PUSH20 ffffffffffffffffffffffffffffffffffffffff ;
0746 AND ;
0747 PUSH20 ffffffffffffffffffffffffffffffffffffffff ;
0768 AND ;
0769 DUP2 ; mem4
0770 MSTORE ; memory[mem4] = msg.sender
0771 PUSH1 20 ;
0773 ADD ; 下一个存储位置 mem5 = mem4+0x20
0774 SWAP1 ;
0775 DUP2 ;
0776 MSTORE ; memory[mem5] = slot2
0777 PUSH1 20 ;
0779 ADD ;
0780 PUSH1 00 ;
0782 SHA3 ; hash 值 sha2 = sha(msg.sender, slot2)
0783 PUSH1 00 ; NameRecord::name 的相对偏移
0785 DUP3 ; record 的位置 slot3
0786 ADD ; 计算 record.name 所在的位置 slot4
0787 SLOAD ; record_name_data = storage[slot4]
0788 DUP2 ;
0789 PUSH1 00 ;
0791 ADD ;
0792 SWAP1 ;
0793 PUSH1 00 ;
0795 NOT ;
0796 AND ;
0797 SWAP1 ;
0798 SSTORE ; storage[sha2] = record_name_data
0799 PUSH1 01 ; NameRecord::mappedAddress 的相对偏移
0801 DUP3 ; slot3
0802 ADD ; record.mappedAddress 所在的位置 slot5
0803 PUSH1 00 ;
0805 SWAP1 ;
0806 SLOAD ; mappedAddress_data = storage[slot5]
0807 SWAP1 ;
0808 PUSH2 0100 ;
0811 EXP ;
0812 SWAP1 ;
0813 DIV ;
0814 PUSH20 ffffffffffffffffffffffffffffffffffffffff ;
0835 AND ;
0836 DUP2 ; sha2
0837 PUSH1 01 ;
0839 ADD ; sha2+1
0840 PUSH1 00 ;
0842 PUSH2 0100 ;
0845 EXP ;
0846 DUP2 ;
0847 SLOAD ; 取出 storage[sha2+1] 位置原数据 sdata2
0848 DUP2 ;
0849 PUSH20 ffffffffffffffffffffffffffffffffffffffff ;
0870 MUL ;
0871 NOT ;
0872 AND ;
0873 SWAP1 ;
0874 DUP4 ; mappedAddress_data
0875 PUSH20 ffffffffffffffffffffffffffffffffffffffff ;
0896 AND ;
0897 MUL ;
0898 OR ;
0899 SWAP1 ;
0900 SSTORE ; storage[sha2+1] = sdata2 & mask | mappedAddress_data
0901 POP ;
0902 SWAP1 ;
0903 POP ;
0904 POP ;
0905 POP ;
0906 POP ;
0907 POP ;
0908 JUMP ;

可以看出 map 中数据的存储位置其实是 map.key 以及 map 所在位置 map_slot 二者共同的 hash 值 sha3(key, map_slot),这个值是一个 uint256。 而 map 无法作为临时变量使用,因此在函数中的临时变量是很难重写到 map 中的位置的。

Array

接下来我们看一下 Array 的情况,修改之前的代码,发现是可以泄露出数组内容的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
contract Locked{
struct Unknow{
uint aba;
uint aha;
uint ale;
uint ati;
uint aqu;
}
uint[3] a = [uint(1), 2, 3];
function GetSome() returns(uint){
Unknow un;
return un.ati;
}
}

调试可以发现全局变量的一个定长 Array 是按照 index 顺序排列在 storage 中的。

同理,如果我们声明一个 Array 作为临时变量则可以读取 storage 中的数据。

接着我们使用 new 关键字申请一个变长数组,查看其运行时存储情况

1
2
3
4
5
6
function GetSome() returns(uint){
stateVar = new uint[](2);
stateVar[1] = 0x10adbeef;
//stateVar = [1,2,4,5,6]; // 这种方式和 new 是一样的
return stateVar[1];
}
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
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
0635 JUMPDEST ;
0636 PUSH1 00 ; stateVar 位置 slot1
0638 PUSH1 02 ; 数组长度 length
0640 PUSH1 40 ;
0: 0x40
1: 0x2
2: 0x0
0642 MLOAD ; 加载 mdata1 = memory[0x40]
0643 SWAP1 ;
0644 DUP1 ;
0645 DUP3 ;
0646 MSTORE ; memory[mdata1] = 2 储存数组大小
0647 DUP1 ;
0648 PUSH1 20 ; 每个数组成员占用的空间
0650 MUL ; 计算数组占用空间 data_size1 = 2 *0x20
0651 PUSH1 20 ; 保留空间?reserve
0653 ADD ; 数组总大小 size1 = data_size1 + reserve
0654 DUP3 ;
0655 ADD ; 计算数组末尾 end1 = mdata1 + size1 ?
0656 PUSH1 40 ;
0658 MSTORE ; memory[0x40] = end1
0659 DUP1 ;
0660 ISZERO ;
0661 PUSH2 02ad ;
0664 JUMPI ; 如果数组长度是为 0 则跳转
0665 DUP2 ;
0666 PUSH1 20 ;
0668 ADD ; 数组起始位置 start1 = mdata1 + reserve
0669 PUSH1 20 ;
0671 DUP3 ;
0672 MUL ; 数组占用空间 data_size1 = 2 *0x20
0673 DUP1 ;
0674 CODESIZE ; 获取当前合约的大小
0675 DUP4 ; start1
0676 CODECOPY ; codecopy(start1, codesize, data_size1) 从代码末尾拷贝 data_size1 大小的数据到 start1 ,不明白这里这样做的原因
0677 DUP1 ; data_size1
0678 DUP3 ; start1
0679 ADD ; end1 = start1+data_size1
0680 SWAP2 ;
0681 POP ;
0682 POP ;
0683 SWAP1 ;
0684 POP ;
0685 JUMPDEST ;
0686 POP ;
0687 PUSH1 00 ; slot1
0689 SWAP1 ;
0690 DUP1 ; mdata1
0691 MLOAD ; memory[mdata1]
0692 SWAP1 ;
0693 PUSH1 20 ;
0695 ADD ; start1 = mdata1 + reserve
0696 SWAP1 ;
0697 PUSH2 02c3 ;
0700 SWAP3 ;
0701 SWAP2 ;
0702 SWAP1 ;
0703 PUSH2 04fb ;
0706 JUMP ; 跳转到数组初始话阶段,从 memory 中循环赋值给 storage
0707 JUMPDEST ;
0708 POP ;
0709 PUSH4 10adbeef ; value
0714 PUSH1 00 ; slot1
0716 PUSH1 01 ; index1
0718 DUP2 ; slot1
0719 SLOAD ; length = storage[slot1]
0720 DUP2 ; index1
0721 LT ;
0722 ISZERO ;
0723 ISZERO ;
0724 PUSH2 02d9 ;
0727 JUMPI ; index1 合法则跳转 0729
0728 INVALID ;
0729 JUMPDEST ;
0730 SWAP1 ;
0731 PUSH1 00 ; mslot
0733 MSTORE ; memory[0] = slot1
0734 PUSH1 20 ;
0736 PUSH1 00 ;
0738 SHA3 ; hash2 = sha3(slot1)
0739 ADD ; hash2 + index 计算数据实际存储的位置
0740 DUP2 ;
0741 SWAP1 ;
0742 SSTORE ; storage[hash2] = value
0743 POP ;
0744 PUSH1 00 ;
0746 PUSH1 01 ;
0748 DUP2 ;
0749 SLOAD ;
0750 DUP2 ;
0751 LT ;
0752 ISZERO ;
0753 ISZERO
0754 PUSH2 02f7
0757 JUMPI
0758 INVALID
0759 JUMPDEST
0760 SWAP1
...
1275 JUMPDEST ;
1276 DUP3 ; slot1
1277 DUP1 ; slot1
1278 SLOAD ; sdata1 = storage[slot1]
1279 DUP3 ; length
1280 DUP3 ; slot1
1281 SSTORE ; storage[slot1] = length ,数组对应的 slot 中存储的只是 slot
1282 SWAP1 ;
1283 PUSH1 00 ; mslot
1285 MSTORE ; memory[0] = slot1
1286 PUSH1 20 ;
1288 PUSH1 00 ;
1290 SHA3 ; hash1 = sha3(slot1)
1291 SWAP1 ;
1292 DUP2 ; hash1
1293 ADD ; hash_o = hash1 + sdata1
1294 SWAP3 ;
1295 DUP3 ; length
1296 ISZERO ;
1297 PUSH2 0537 ;
1300 JUMPI ; 如果数组长度是为 0 则跳转
1301 SWAP2 ;
1302 PUSH1 20 ;
1304 MUL ; 数组占用空间 array_data_size1 = 2 *0x20
1305 DUP3 ; start1
1306 ADD ; end1 = start1 + array_data_size1
1307 JUMPDEST ;
1308 DUP3 ; start1
1309 DUP2 ; end1
1310 GT ;
1311 ISZERO ;
1312 PUSH2 0536 ;
1315 JUMPI ; 如果 end < start 则跳转 1334 退出循环
1316 DUP3 ; start1
1317 MLOAD ; mdata2 = memory[start1]
1318 DUP3 ; hash1
1319 SSTORE ; storage[hash1] = mdata2
1320 SWAP2 ;
1321 PUSH1 20 ;
1323 ADD ; start1 + 0x20
1324 SWAP2 ;
1325 SWAP1 ;
1326 PUSH1 01 ;
1328 ADD ; hash1 + 1
1329 SWAP1 ;
1330 PUSH2 051b ; 是一循环跳回 1307 这里
1333 JUMP ;
1334 JUMPDEST ;
1335 JUMPDEST ;
1336 POP ;
1337 SWAP1 ;
1338 POP ;
1339 PUSH2 0544 ;
1342 SWAP2 ;
1343 SWAP1 ;
1344 PUSH2 0548 ;
1347 JUMP ;
1348 JUMPDEST ;
1349 POP ;
1350 SWAP1 ;
1351 JUMP ;
1352 JUMPDEST ;
1353 PUSH2 056a ;
1356 SWAP2 ;
1357 SWAP1 ;
1358 JUMPDEST ;
1359 DUP1 ;
1360 DUP3 ;
1361 GT ;
1362 ISZERO ;
1363 PUSH2 0566 ; 如果 循环赋值之后 的 hash 大于 原本的hash 则跳转
1366 JUMPI ;
```
可以看到如果是一个变长数组,数组成员的存储位置就是根据 hash 值来选定的了, 数组的存储位置为 `sha3(数组对象位置)+index`。数组本身那个 slot 中所存放的只是数组的长度而已,这样也就很好理解为什么存放在 storage 中的变长数组可以通过调整 length 属性来自增。
多维数组的情况与简单数组类似。(注意!!! Solidity 的多维数组声明和 C++ 中的习惯是相反的!!!但是使用时是相同的!!!这就对程序员提出了比较高的要求,一不小心就可能写错!!!)
```js
function GetSome() returns(uint){
uint[2][3] stateVar; // 声明一个 3 行 2列的数组
return stateVar[0][1]; // 访问第 0 行第 1 列的成员
}

数组+结构体

如果数组和结构体组合起来,那么数据在 storage 中的索引将如何确定呢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
contract Project
{
struct Person {
address[] addr;
uint funds;
}
mapping(address => Person) public people;
function f() {
Person p;
p.addr = [0xca35b7d915458ef540ade6068dfe2f44e8fa733c,0x14723a09acff6d2a60dcdf7aa4aff308fddc160c];
p.funds = 0x10af;
people[msg.sender] = p;
}
}

我们写一个程序做测试,由于篇幅的关系,不再贴上 opcode 做说明。 Person 类型的对象 p 第一个成员是一个动态数组 addr,存储 p 对象时,首先存储动态数组:storage[hash(msg_sender,people_slot)] = storage[p+slot],接着依次存储动态数组内容:storage[hash(hash(msg_sender,people_slot))] = storage[hash(p_slot)]; storage[hash(hash(msg_sender,people_slot))+1)] = storage[hash(p_slot)+1]; 最后存储 funds:storage[hash(msg_sender,people_slot)+1]

同理,数组中的结构体存储也是类似。

原因分析

我们下载 solidity 编译器的源码进行查看,分析这里出现问题的原因。源码可在这里 找到,直接使用 cmake 编译源码即可,编译教程。 solidity 的源码需要引用 boost 库,如果之前没有安装的话需要先安装 boost。编译的过程不再赘述,最终会生成三个可执行文件 (在 Windows 上的编译会有点问题,依赖的头文件没办法自动加入工程,需要手动添加,并且会还有一些字符表示的问题)

  • solc\solc
  • lllc\lllc
  • test\soltest

solc 可以将 sol 源码编译成 EVM 可以运行的 bytecode ,lllc 不知道是干什么的~

调试 Solc ,查看其中对于 struct 作为临时变量时的编译情况

1
2
3
4
5
6
7
8
9
10
11
12
contract Project
{
uint a= 12345678;
struct Leak{
uint s1;
}
function f(uint i) returns(uint) {
Leak l;
return l.s1;
}
}

关键代码调用栈如下

1
2
3
4
5
6
7
8
9
10
> solc.exe!dev::solidity::ContractCompiler::appendStackVariableInitialisation(const dev::solidity::VariableDeclaration & _variable) Line 951 C++
solc.exe!dev::solidity::ContractCompiler::visit(const dev::solidity::FunctionDefinition & _function) Line 445 C++
solc.exe!dev::solidity::FunctionDefinition::accept(dev::solidity::ASTConstVisitor & _visitor) Line 206 C++
solc.exe!dev::solidity::ContractCompiler::appendMissingFunctions() Line 870 C++
solc.exe!dev::solidity::ContractCompiler::compileContract(const dev::solidity::ContractDefinition & _contract, const std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _contracts) Line 75 C++
solc.exe!dev::solidity::Compiler::compileContract(const dev::solidity::ContractDefinition & _contract, const std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _contracts, const std::vector<unsigned char,std::allocator<unsigned char> > & _metadata) Line 39 C++
solc.exe!dev::solidity::CompilerStack::compileContract(const dev::solidity::ContractDefinition & _contract, std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _compiledContracts) Line 730 C++
solc.exe!dev::solidity::CompilerStack::compile() Line 309 C++
solc.exe!dev::solidity::CommandLineInterface::processInput() Line 837 C++
solc.exe!main(int argc, char * * argv) Line 59 C++

关键函数为 appendStackVariableInitialisation,可以看到这里调用 pushZeroValue 记录临时变量信息,如果函数发现 value 存在于 Storage 中,那么就直接 PUSH 0,直接压入 0!!!所有的临时变量都通过这条路径,换而言之,所有的临时变量 slot 都是 0 !!!

1
2
3
4
5
6
void ContractCompiler::appendStackVariableInitialisation(VariableDeclaration const& _variable)
{
CompilerContext::LocationSetter location(m_context, _variable);
m_context.addVariable(_variable);
CompilerUtils(m_context).pushZeroValue(*_variable.annotation().type);
}

笔者目前还不能理解这样设计的原因,猜测可能是因为 storage 本身稀疏数组的关系,不便于通过其他额外变量来控制 slot 位置,但是以目前这样的实现,其问题应该更多。

与之相对的全局变量的编译,函数调用栈如下

1
2
3
4
5
6
7
8
9
> solc.exe!dev::solidity::ContractCompiler::initializeStateVariables(const dev::solidity::ContractDefinition & _contract) Line 403 C++
solc.exe!dev::solidity::ContractCompiler::appendInitAndConstructorCode(const dev::solidity::ContractDefinition & _contract) Line 146 C++
solc.exe!dev::solidity::ContractCompiler::packIntoContractCreator(const dev::solidity::ContractDefinition & _contract) Line 165 C++
solc.exe!dev::solidity::ContractCompiler::compileConstructor(const dev::solidity::ContractDefinition & _contract, const std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _contracts) Line 89 C++
solc.exe!dev::solidity::Compiler::compileContract(const dev::solidity::ContractDefinition & _contract, const std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _contracts, const std::vector<unsigned char,std::allocator<unsigned char> > & _metadata) Line 44 C++
solc.exe!dev::solidity::CompilerStack::compileContract(const dev::solidity::ContractDefinition & _contract, std::map<dev::solidity::ContractDefinition const *,dev::eth::Assembly const *,std::less<dev::solidity::ContractDefinition const *>,std::allocator<std::pair<dev::solidity::ContractDefinition const * const,dev::eth::Assembly const *> > > & _compiledContracts) Line 730 C++
solc.exe!dev::solidity::CompilerStack::compile() Line 309 C++
solc.exe!dev::solidity::CommandLineInterface::processInput() Line 837 C++
solc.exe!main(int argc, char * * argv) Line 59 C++

关键函数为 StorageItem::StorageItem ,函数从 storageLocationOfVariable 中获取全局变量在 storage 中的 slot

1
2
3
4
5
6
StorageItem::StorageItem(CompilerContext& _compilerContext, VariableDeclaration const& _declaration):
StorageItem(_compilerContext, *_declaration.annotation().type)
{
auto const& location = m_context.storageLocationOfVariable(_declaration);
m_context << location.first << u256(location.second);
}

总结

综上所述,由于 solidity 编译器实现的问题,在智能合约中使用 struct 对象有可能导致变量覆盖,重写了某些关键的全局变量。

但这也不意味着只要使用了 struct 的智能合约都会出现问题,经过我们对目前 ETH 中运行的 20000 多个智能合约的排查,大部分使用了 struct 的智能合约都没有直接使用未初始化的 struct 变量。几种典型的情况如下所示

1
2
3
4
5
6
7
8
9
10
11
12
13
struct A{
a;
b;
}
mapping( xxx => A) map;
map[x] = A(); // A() 在 memory 中初始化
map[x].a = yy; // 直接像 storage 中的 hash 位置赋值
map[x].b = zz;
A a = map[x]; // 经过编译优化之后,操作 a 相当于直接操作 map 中的数据,直接访问 map 中的数据位置

在目前现有的合约中,尚未发现变量覆盖的真实攻击案例,但是作为一个严重的设计缺陷,只能合约的开发者还是应该时刻警惕,避免产生不可预知的问题。

Reference

[1] https://github.com/ethereum/solidity/issues/1550
[2] https://lilymoana.github.io/ethereum_theory.html
[3] https://github.com/ethereum/solidity
[4] http://solidity.readthedocs.io/en/v0.4.24/installing-solidity.html