0%

通过 EVM PUZZLES 学习 EVM opcodes

Evm puzzles 是一套练习和入门 evm 执行原理和 opcode 的习题,里边涉及到简单的 opcode 操作,如操作堆栈,操作内存,操作 calldata ,部署合约等等,更重要的是它只有十道题,即使是新手也可以在几个小时内解决谜题!让我们开始吧!

规则:输入恰当的 calldata 和 value,使得题目的 opcode 正确执行,直到执行STOP。

puzzle 交互工具:https://github.com/fvictorio/evm-puzzles

puzzle 1:

题目:

1
2
3
4
5
6
7
8
9
10
11
pc      opcode  opcode name
00 34 CALLVALUE
01 56 JUMP
02 FD REVERT
03 FD REVERT
04 FD REVERT
05 FD REVERT
06 FD REVERT
07 FD REVERT
08 5B JUMPDEST
09 00 STOP

此题简单明了,我们需要跳转至pc = 0x08才能避免中途 revert,所以0x01位置的JUMP应该跳转至 0x08。 根据evm文档,JUMP 操作 的模式为:

stack name gas initial stack result stack mem/storage
56 JUMP 8 dst / /
34 CALLVALUE 2 / msg.value /

即:CALLVALUE 把 msg.value 放在调用栈上,JUMP 从调用栈的一个值,跳转至该值位置。 现在应该很清楚了 msg.value == 8 即可通关。

puzzle 2:

题目:

1
2
3
4
5
6
7
8
9
10
11
pc      opcode  opcode name
00 34 CALLVALUE
01 38 CODESIZE
02 03 SUB
03 56 JUMP
04 FD REVERT
05 FD REVERT
06 5B JUMPDEST
07 00 STOP
08 FD REVERT
09 FD REVERT

此题跟 puzzle 1 差不多,只新增了两个 opcode,我们依然先看文档:

stack name gas initial stack result stack mem/storage
38 CODESIZE 2 / len(this.code) /
03 SUB 3 a,b a - b /

我们先使用 CALLVALUE 将 msg.value 压入调用栈,再把代码长度压栈,我们可以看到我们的 code 总长度为10,也就是 10 - msg.value == 0x06,可得 msg.value == 4。题解结束。

puzzle 3:

题目:

1
2
3
4
5
6
7
pc      opcode  opcode name
00 36 CALLDATASIZE
01 56 JUMP
02 FD REVERT
03 FD REVERT
04 5B JUMPDEST
05 00 STOP

新增了一个 opcode:

stack name gas initial stack result stack mem/storage
36 CALLDATASIZE 2 / len(msg.data) /

只要我们的 len(msg.data) == 4 即可完成题解,例如 msg.data == 0x00000000

puzzle 4:

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
pc      opcode  opcode name
00 34 CALLVALUE
01 38 CODESIZE
02 18 XOR
03 56 JUMP
04 FD REVERT
05 FD REVERT
06 FD REVERT
07 FD REVERT
08 FD REVERT
09 FD REVERT
0A 5B JUMPDEST
0B 00 STOP

XOR 大家应该都很熟悉了, 就是 pop 两个操作栈里的值,然后把异或结果 pop 回去:

stack name gas initial stack result stack mem/storage
18 XOR 3 a,b a ^ b /

我们需要保证 msg.value xor codesize == 0x0A。0x0A 的二进制表示为 0000 1010,而我们可以知道 codesize == 0c(10进制的12),二进制表示为0000 1100,可以计算出 msg.data == 6 即二进制表示为 0000 0110。

puzzle 5:

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
pc      opcode      opcode name
00 34 CALLVALUE
01 80 DUP1
02 02 MUL
03 610100 PUSH2 0100
06 14 EQ
07 600C PUSH1 0C
09 57 JUMPI
0A FD REVERT
0B FD REVERT
0C 5B JUMPDEST
0D 00 STOP
0E FD REVERT
0F FD REVERT

认识一下新的 opcode:

stack name gas initial stack result stack mem/storage
80 DUP1 3 a a,a /
02 MUL 5 a,b a * b /
60 PUSH1 3 . uint8 /
61 PUSH2 3 . uint16 /
14 EQ 3 a,b a == b /
57 JUMPI 10 dst, condition / /

DUP1:将栈顶元素再复制一份放在栈顶。 MUL:pop 栈顶两元素,push a*b 的结果到栈顶。 PUSH1/PUSH2:把一个元素放在栈顶,如610100 即使用 PUSH2(61) 将 8(0100) 放在栈顶。 EQ:pop 栈顶两元素,push a==b 的结果到栈顶。 JUMPI:取栈顶两元素,分别是 dst 和 condition,如果 condition == 1 则跳转到 pc == dst的位置,否则,执行下一条语句。

我们可以从结果往前推,目的是达到0x0d位置的 STOP,需要通过跳转达到0x0c,也就是来自 pc == 0x09位置的 JUMPI 。 pc == 0x07 处我们得知,此时的栈顶元素是 PUSH1 操作符操作的 0x0c,这正好与我们的目标地址相等。也就是说明 JU MPI 的 condition 为真。即pc == 0x06处的 EQ 返回为真。 再往上就简单多了 ,其实只需要满足 msg.value*msg.value == 0x0100 注意,这里的0100是16进制数字而非二进制数字,所以msg.data*msg.data == 256msg.data == 16 完成题解。

puzzle 6:

题目:

1
2
3
4
5
6
7
8
9
10
11
12
pc      opcode  opcode name
00 6000 PUSH1 00
02 35 CALLDATALOAD
03 56 JUMP
04 FD REVERT
05 FD REVERT
06 FD REVERT
07 FD REVERT
08 FD REVERT
09 FD REVERT
0A 5B JUMPDEST
0B 00 STOP

新增一个 opcode:

stack name gas initial stack result stack mem/storage
35 CALLDATALOAD 3 idx msg.data[idx:idx+32] /

简单明了 pop 栈顶元素作为 idx ,把 msg.data 从 idx 开始的32位写入操作栈。 这样我们可以直接获得构造条件:msg.data[0:32]== 0x000000000000000000000000000000000000000000000000000000000000000a 即可

puzzle 7:

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
pc      opcode    opcode name
00 36 CALLDATASIZE
01 6000 PUSH1 00
03 80 DUP1
04 37 CALLDATACOPY
05 36 CALLDATASIZE
06 6000 PUSH1 00
08 6000 PUSH1 00
0A F0 CREATE
0B 3B EXTCODESIZE
0C 6001 PUSH1 01
0E 14 EQ
0F 6013 PUSH1 13
11 57 JUMPI
12 FD REVERT
13 5B JUMPDEST
14 00 STOP

从第7题开始难度会增加一些,我们先看一下新引入的opcode吧

stack name gas initial stack result stack mem/storage
37 CALLDATACOPY 3+ dstOst, ost, len / mem[dstOst:dstOst+len-1] := msg.data[ost:ost+len-1]
F0 CREATE 32000+ val, ost, len addr /
3B EXTCODESIZE 100/2600 addr len(addr.code) /

CALLDATACOPY 简单明了,就是 pop 出三个栈顶元素,记为dstOst, ost, len。把msg.data[ost:ost+len-1] 复制到内存:msg.data[ost:ost+len-1]

CREATE 相对复杂,它是一个部署合约的 opcode,pop 出三个栈顶元素,记为val, ost, len。 val 是创建合约时传入的 eth 数目。 mem[ost:ost+len-1] 是合约的 contract code。 addr 返回值是已创建合约的地址。

EXTCODESIZE 取出栈顶元素,然后返回该合约的 code 长度。

我们先看第一段,0x00 - 0x05

1
2
3
4
5
6
pc      opcode    opcode name
00 36 CALLDATASIZE
01 6000 PUSH1 00
03 80 DUP1
04 37 CALLDATACOPY
05 36 CALLDATASIZE

这一段其实就是把 msg.data 整个复制到内存mem

第二段,0x06-0x0a

1
2
3
4
pc      opcode  opcode name
06 6000 PUSH1 00
08 6000 PUSH1 00
0A F0 CREATE

mem上的代码部署到链。并把合约地址写回栈顶。

第三段,0x0b-0x14

1
2
3
4
5
6
7
8
9
pc      opcode    opcode name
0B 3B EXTCODESIZE
0C 6001 PUSH1 01
0E 14 EQ
0F 6013 PUSH1 13
11 57 JUMPI
12 FD REVERT
13 5B JUMPDEST
14 00 STOP

我们可以看到要正确跳转需要满足的条件是 len(address.code) == 1

现在的问题是,新创建合约使用的code是我们的 msg.data,那是不是意味着部署后合约的 code 也是msg.data 呢?其实不然。创建合约的 code 被称为 creation code, 而最终留在区块链里的合约代码被称为 runtime code。这其实隐含着 conrtuctor的内容,contructor 只存在于 creation code 而非 runtime code ,这也是构造函数只被执行一次的原因。合约 creation code 会在一个交易里执行,并把 runtime code通过 RETURN 返回。

我们还需要额外引入一个 opcode:

stack name gas initial stack result stack mem/storage
F3 RETURN * ost, len / /

ost 为 runtime code 在内存的起始位置,len 为截取长度,我们尝试构建一个 creation code:

1
2
3
4
pc      opcode    opcode name
00 6001 PUSH1 01
02 6000 PUSH1 00
04 F3 RETURN

这样我们返回的 runtime code 的长度就只是从内存中取出的一位操作符了。

也就是 msg.data == 0x60016000f3 即可完成 puzzle !

puzzle 8:

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pc      opcode    opcode name
00 36 CALLDATASIZE
01 6000 PUSH1 00
03 80 DUP1
04 37 CALLDATACOPY
05 36 CALLDATASIZE
06 6000 PUSH1 00
08 6000 PUSH1 00
0A F0 CREATE
0B 6000 PUSH1 00
0D 80 DUP1
0E 80 DUP1
0F 80 DUP1
10 80 DUP1
11 94 SWAP5
12 5A GAS
13 F1 CALL
14 6000 PUSH1 00
16 14 EQ
17 601B PUSH1 1B
19 57 JUMPI
1A FD REVERT
1B 5B JUMPDEST
1C 00 STOP

难度比起 puzzle 7 更上一层楼,我们先看看涉及到了几个新的 opcode:

stack name gas initial stack result stack mem/storage
94 SWAP5 3 a, ..., b b, ..., a /
5A GAS 2 / gasRemaining /
F1 CALL base_gas + gas_sent_with_call gas, addr, val, argOst, argLen, retOst, retLen success mem[retOst:retOst+retLen-1] := returndata

SWAP 系列的 opcode 很好理解,SWAP1 是把栈顶的 a,b 变成 b,a,SWAP5 就是把 a,x,x,x,x,,b 变成 b,x,x,x,x,a。 GAS 是计算 gas 费并写入栈顶。 CALL 即为调用合约,其使用到的栈顶元素 分别为 gas, addr(合约地址), val(msg.data), argOst(在内存中截取的输入数据起点), argLen(输入数据长度), retOst(在内存中截取的返回数据起点), retLen(返回数据长度)。若交易成功 将 1 写入栈顶,若失败,将0写入栈顶。

我们先看第一段:

1
2
3
4
5
6
7
8
9
pc      opcode    opcode name
00 36 CALLDATASIZE
01 6000 PUSH1 00
03 80 DUP1
04 37 CALLDATACOPY
05 36 CALLDATASIZE
06 6000 PUSH1 00
08 6000 PUSH1 00
0A F0 CREATE

z这一段跟 puzzle 7 一样,把我们 msg.data 的数据当成合约部署在链上,再把新创建的合约地址写回栈顶。

第二段:

1
2
3
4
5
6
7
8
9
pc      opcode    opcode name
0B 6000 PUSH1 00
0D 80 DUP1
0E 80 DUP1
0F 80 DUP1
10 80 DUP1
11 94 SWAP5
12 5A GAS
13 F1 CALL

这一段运行下到最后,我们可以知道这是一个 call 命令,参数依次是 gas , addr , 0, 0, 0, 0, 0。也就是不传入数据,没有返回值,不传入value, 若运行成功则把 1 压栈,反之压栈 0。

第三段

1
2
3
4
5
6
7
8
pc      opcode    opcode name
14 6000 PUSH1 00
16 14 EQ
17 601B PUSH1 1B
19 57 JUMPI
1A FD REVERT
1B 5B JUMPDEST
1C 00 STOP

这一段就是用来反推我们合约返回结果的部分了,我们需要从上一个部分得到一个 0x00,使得EQ返回值为 1,方能成功触发 JUMPI 跳到终点。也就说明我们需要本次合约交易失败。这其实很简单,我们构造一个合约使得 runtime code只有一个 revert 就可以了,类似 puzzle 7,再稍微加点东西:

1
2
3
4
5
6
7
pc      opcode    opcode name
00 60FD PUSH1 FD //FD 是revert操作符的编号
02 6000 PUSH1 00
04 53 MSTORE8
05 6001 PUSH1 01
07 6000 PUSH1 00
09 F3 RETURN

这样,我们把 0xfd 写入内存 0x00了。这也就是我们的 runtime code。

这样我们得出题解:msg.data == 0x60fd60005360016000f3

puzzle 9:

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
pc      opcode    opcode name
00 36 CALLDATASIZE
01 6003 PUSH1 03
03 10 LT
04 6009 PUSH1 09
06 57 JUMPI
07 FD REVERT
08 FD REVERT
09 5B JUMPDEST
0A 34 CALLVALUE
0B 36 CALLDATASIZE
0C 02 MUL
0D 6008 PUSH1 08
0F 14 EQ
10 6014 PUSH1 14
12 57 JUMPI
13 FD REVERT
14 5B JUMPDEST
15 00 STOP

经历了 puzzle 7 && puzzle 8 的折磨,接下来的两道题实际上已经难不住我们了,先看新出现的 opcode 文档,非常简单:

stack name gas initial stack result stack mem/storage
10 LT 3 a, b a < b /
02 MUL 5 a,b a * b /

第一段:

1
2
3
4
5
6
7
8
9
pc      opcode    opcode name
00 36 CALLDATASIZE
01 6003 PUSH1 03
03 10 LT
04 6009 PUSH1 09
06 57 JUMPI
07 FD REVERT
08 FD REVERT
09 5B JUMPDEST

我们可以得出一个限制:0x03 < len(msg.data)

第二段:

1
2
3
4
5
6
7
8
9
10
11
pc      opcode    opcode name
0A 34 CALLVALUE
0B 36 CALLDATASIZE
0C 02 MUL
0D 6008 PUSH1 08
0F 14 EQ
10 6014 PUSH1 14
12 57 JUMPI
13 FD REVERT
14 5B JUMPDEST
15 00 STOP

很容易得出另一个限制 len(msd.data) * msg.value == 8, 又根据第一部分的限制,我们可以随意构造一个长度为4的m sg.data,然后令 msg.value == 2。

即可构造出一个符合题意的题解: msg.value == 2 msg.data == 0x12121212

puzzle 10:

题目:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
pc      opcode      opcode name
00 38 CODESIZE
01 34 CALLVALUE
02 90 SWAP1
03 11 GT
04 6008 PUSH1 08
06 57 JUMPI
07 FD REVERT
08 5B JUMPDEST
09 36 CALLDATASIZE
0A 610003 PUSH2 0003
0D 90 SWAP1
0E 06 MOD
0F 15 ISZERO
10 34 CALLVALUE
11 600A PUSH1 0A
13 01 ADD
14 57 JUMPI
15 FD REVERT
16 FD REVERT
17 FD REVERT
18 FD REVERT
19 5B JUMPDEST
1A 00 STOP

和第九题很类似,我们看一下新增的几个 opcode,也都很简单:

stack name gas initial stack result stack mem/storage
38 CODESIZE 2 / len(this.code) /
11 GT 3 a,b a > b /
01 ADD 3 a,b a + b /
06 MOD 5 a,b a % b /
15 ISZERO 3 a a == 0 /

值得注意的是,codesize是指我们正在运行的代码的长度,本题中我们可以看到是 21 (0x1b)。

第一段:

1
2
3
4
5
6
7
8
9
pc      opcode      opcode name
00 38 CODESIZE
01 34 CALLVALUE
02 90 SWAP1
03 11 GT
04 6008 PUSH1 08
06 57 JUMPI
07 FD REVERT
08 5B JUMPDEST

我们得到第一条限制 21 > msg.value

第二段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
pc      opcode      opcode name
09 36 CALLDATASIZE
0A 610003 PUSH2 0003
0D 90 SWAP1
0E 06 MOD
0F 15 ISZERO
10 34 CALLVALUE
11 600A PUSH1 0A
13 01 ADD
14 57 JUMPI
15 FD REVERT
16 FD REVERT
17 FD REVERT
18 FD REVERT
19 5B JUMPDEST
1A 00 STOP

我们从结果反推0x14处, JUMPI 的 dst 操作数 应为 0x19指向0x19处的JUMPDEST,condition 操作数应为 1。 这其实就是 0x13处 ADD 的结果为 0x190x0f处的 ISZERO 结果为 1。

len(msg.data) % 3 == 0msg.data + 10 == 25(0x0A的十进制是10,0x19的十进制是25)。

我们可以构造一个题解: msg.value == 15 msg.data == 0x121212