Evm puzzles 是一套练习和入门 evm 执行原理和 opcode 的习题,里边涉及到简单的 opcode 操作,如操作堆栈,操作内存,操作 calldata ,部署合约等等,更重要的是它只有十道题,即使是新手也可以在几个小时内解决谜题!让我们开始吧!
规则:输入恰当的 calldata 和 value,使得题目的 opcode 正确执行,直到执行STOP。
puzzle 交互工具:https://github.com/fvictorio/evm-puzzles
puzzle 1:
题目:
1 | pc opcode opcode name |
此题简单明了,我们需要跳转至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 | pc opcode opcode name |
此题跟 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 | pc opcode opcode name |
新增了一个 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 | pc opcode opcode name |
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 | pc opcode opcode name |
认识一下新的 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 == 256
即 msg.data == 16
完成题解。
puzzle 6:
题目:
1 | pc opcode opcode name |
新增一个 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 | pc opcode opcode name |
从第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 | pc opcode opcode name |
这一段其实就是把 msg.data
整个复制到内存mem
。
第二段,0x06
-0x0a
1 | pc opcode opcode name |
把 mem
上的代码部署到链。并把合约地址写回栈顶。
第三段,0x0b
-0x14
1 | pc opcode opcode name |
我们可以看到要正确跳转需要满足的条件是
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 | pc opcode opcode name |
这样我们返回的 runtime code 的长度就只是从内存中取出的一位操作符了。
也就是 msg.data == 0x60016000f3
即可完成 puzzle !
puzzle 8:
题目:
1 | pc opcode opcode name |
难度比起 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 | pc opcode opcode name |
z这一段跟 puzzle 7 一样,把我们 msg.data 的数据当成合约部署在链上,再把新创建的合约地址写回栈顶。
第二段:
1 | pc opcode opcode name |
这一段运行下到最后,我们可以知道这是一个 call 命令,参数依次是 gas , addr , 0, 0, 0, 0, 0。也就是不传入数据,没有返回值,不传入value, 若运行成功则把 1 压栈,反之压栈 0。
第三段
1 | pc opcode opcode name |
这一段就是用来反推我们合约返回结果的部分了,我们需要从上一个部分得到一个
0x00
,使得EQ返回值为 1,方能成功触发 JUMPI
跳到终点。也就说明我们需要本次合约交易失败。这其实很简单,我们构造一个合约使得
runtime code只有一个 revert 就可以了,类似 puzzle
7,再稍微加点东西:
1 | pc opcode opcode name |
这样,我们把 0xfd
写入内存
0x00
了。这也就是我们的 runtime code。
这样我们得出题解:msg.data == 0x60fd60005360016000f3
puzzle 9:
题目:
1 | pc opcode opcode name |
经历了 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 | pc opcode opcode name |
我们可以得出一个限制:0x03 < len(msg.data)
第二段:
1 | pc opcode opcode name |
很容易得出另一个限制 len(msd.data) * msg.value == 8
,
又根据第一部分的限制,我们可以随意构造一个长度为4的m sg.data,然后令
msg.value == 2。
即可构造出一个符合题意的题解: msg.value == 2
msg.data == 0x12121212
puzzle 10:
题目:
1 | pc opcode opcode name |
和第九题很类似,我们看一下新增的几个 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 | pc opcode opcode name |
我们得到第一条限制 21 > msg.value
第二段:
1 | pc opcode opcode name |
我们从结果反推0x14
处, JUMPI 的 dst 操作数 应为
0x19
指向0x19
处的JUMPDEST,condition 操作数应为
1。 这其实就是 0x13
处 ADD 的结果为
0x19
,0x0f
处的 ISZERO 结果为 1。
即
len(msg.data) % 3 == 0
,msg.data + 10 == 25
(0x0A的十进制是10,0x19的十进制是25)。
我们可以构造一个题解: msg.value == 15
msg.data == 0x121212