EVM-Puzzles
挑战仓库地址
EVM操作码
准备工作:
- 安装hardhat
- 克隆仓库,进入仓库根目录
- 准备环境:npm install yarn
- 开始挑战:npx hardhat play
Puzzle 1
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ############ # Puzzle 1 # ############
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
? Enter the value to send: (0)
|
首先,我们需要知道 CALLVALUE 的作用。此操作码获取 wei 中当前调用的值(即事务值),并将该值推送到堆栈的顶部。
即如果我们输入4,堆栈会发生如下变化
1 2 3
| [0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0] ---> [4 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
接下来,我们需要知道 JUMP 指令的作用。此操作码消耗堆栈上的顶部值,并跳转到序列中的第 th 条指令。
即如果我们输入4,那么将会执行位于04的操作码
执行REVERT,运行失败
因此我们输入8,将会执行JUMPDEST,跳过所有指令
Puzzle 2
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| ############ # Puzzle 2 # ############
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
? Enter the value to send: (0)
|
我们的目标是使栈顶的值为6,使程序跳过所有REVERT
CODESIZE 获取在当前环境中运行的代码的大小,并入栈。在此示例中,我们可以通过查看序列中有多少操作码来手动检查代码的大小。每个操作码是1个字节,在这个谜题中,我们有10个操作码,这意味着代码的大小是10个字节。注意,EVM 使用十六进制数来表示字节码。所以这里是0a
SUB 会把第一个堆栈元素减去第二个堆栈元素,并将结果推送到堆栈的顶部。
此时的堆栈情况
1
| [input 0a 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
即 0a - input = 6
所以输入4
Puzzle 3
1 2 3 4 5 6 7 8 9 10 11 12 13
| ############ # Puzzle 3 # ############
00 36 CALLDATASIZE 01 56 JUMP 02 FD REVERT 03 FD REVERT 04 5B JUMPDEST 05 00 STOP
? Enter the calldata:
|
CALLDATASIZE 会获取调用数据的大小(以字节为单位),并将其推送到堆栈上。
并且题目要求我们输入calldata
所以我们输入四个指令的字节码,使 CALLDATASIZE 指令在堆栈上推送4
这里我输入了 0x5B5B5B5B
Puzzle 4
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| ############ # Puzzle 4 # ############
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
|
这里的 CODESIZE 为12,即0c
此时堆栈状态
1
| [0c input 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
XOR 以二进制表示形式计算两个数字,两者相等为0,不等为1
假设我们在堆栈的顶部有两个数字。
1
| [5 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
在执行 XOR 指令时,我们可以想象二进制表示中的两个数字是这样的。
1 2
| 00000000000000000000000000000101 00000000000000000000000000000011
|
得到
1
| 00000000000000000000000000000110
|
而要想通过,我们需要得到0a
1 2
| 0c XOR input = 0a 1100 XOR input = 1010
|
所以输入0110,即6
Puzzle 5
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| ############ # Puzzle 5 # ############
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
? Enter the value to send: (0)
|
DUP1 会把堆栈上的第一个位置上的值复制,并将副本推送到堆栈的顶部。
所以现在在前两条指令之后,我们的堆栈看起来像这样。
1
| [input input 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
MUL 获取堆栈上的前两个值,将它们相乘,然后将结果推送到堆栈的顶部。
因此,在本例中,MUL以后生成的堆栈如下所示。
1
| [mulResult 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
PUSH2 会将指定的 2 字节值推送到堆栈的顶部。这里指定了0100(十六进制)
1
| [0100 mulResult 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
EQ 会获取堆栈上的前两个值,将其进行比较并将结果推送到堆栈顶部。如果前两个值相等,则为1,否则为0。
JUMPI 将有条件地更改程序计数器。此指令查看第二个堆栈元素以了解它是否应该跳转,具体取决于第二个堆栈元素是 1 还是 0 。然后使用第一个堆栈元素来知道要跳转到哪个位置。
因此此时我们的堆栈应该是这样
1
| [0c 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
即跳转到第12条命令,通过挑战
所以为了使 EQ 的结果为1,我们需要使 input * input 的结果等于十六进制的0100,也就是256
所以输入16
Puzzle 6
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ############ # Puzzle 6 # ############
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
? Enter the calldata:
|
CALLDATALOAD 从调用数据的给定偏移量开始的 32 字节值,不足 32 bytes 的自动补0
例如
1 2 3
| calldata 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF i 31 output 0xFF00000000000000000000000000000000000000000000000000000000000000
|
而如果输入 0x0A 会变成 0A00000000000000000000000000000000000000000000000000000000000000
所以应该输入0x000000000000000000000000000000000000000000000000000000000000000a
Puzzle 7
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
|
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
|
CALLDATASIZE
1
| [calldatasize 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
PUSH1 00
1
| [0 calldatasize 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
DUP1
1
| [0 0 calldatasize 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
CALLDATACOPY destOffset offset size
从第 offset 的位置复制长度为 size 的 calldata,并从 destOffset 写进 memory。
在此过程将消耗堆栈上的所有三个顶部元素
运行后的结果和之前没有变化
1
| [0 0 calldatasize 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
CALLDATASIZE
PUSH1 00
PUSH1 00
1
| [0 0 calldatasize 0 0 calldatasize 0 0 0 0 0 0 0 0 0 0]
|
CREATE value offset size
建立(部署)新合约,会开启子 context 并执行初始化 code,执行完后会继续 context。
如果部署成功,新地址的 code 会是执行初始化 code 的回传结果。
合约地址的计算公式:
1
| address = keccak256(rlp([sender_address,sender_nonce]))[12:]
|
value
发送给合约的 value。
offset|size
将 memory 里第 offset 长度为 size 的资料作为初始化 code 赋给 EVM 执行。
因此运行结果
1
| [address_deployed 0 0 calldatasize 0 0 0 0 0 0 0 0 0 0 0 0]
|
EXTCODESIZE 会将堆栈顶部的地址的代码大小返回
1
| [address_code_size 0 0 calldatasize 0 0 0 0 0 0 0 0 0 0 0 0]
|
PUSH1 01
EQ
1
| [1 address_code_size 0 0 calldatasize 0 0 0 0 0 0 0 0 0 0 0 ]
|
然后比较01和address_code_size是否相等
若相等则可以成功跳转至第13条命令,通过挑战
因此现在的目标是使输入的calldata的calldatasize为1
RETURN 的作用:从堆栈中弹出 2 个值以将它们用作以下操作的输入
- offset:从哪里开始读取的内存偏移量
- size:要读取和返回的内存大小(以字节为单位)
因此,无论它在内存中是什么,我们都只想返回 1 条 1 字节的指令。我们的目标是执行RETURN(offset=0, size=1)
。
所以一个简单的智能合约
1 2 3 4 5 6
| 6000 PUSH1 00 // 00 是 STOP 6000 PUSH1 00 // 这将被用作 MSTORE8 的偏移量,在内存中存储 STOP 53 MSTORE8 // 将从偏移量 0 开始在内存中存储 `00` 值(来自第一个 PUSH1) 6001 PUSH1 01 // 必须返回 1 字节 6000 PUSH1 00 // 从 0 返回这些字节 F3 RETURN
|
翻译为字节码即为 600060005360016000F3
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 25 26 27 28 29 30
| ############ # Puzzle 8 # ############
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
? Enter the value to send: (0)
|
前面都和第七题一样
1 2 3 4 5 6 7 8 9 10
| 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
[address_deployed 0 0 calldatasize 0 0 0 0 0 0 0 0 0 0 0 0]
|
接下来的几条指令
1 2 3 4 5 6 7
| 0B 6000 PUSH1 00 0D 80 DUP1 0E 80 DUP1 0F 80 DUP1 10 80 DUP1
[0 0 0 0 0 address_deployed 0 0 calldatasize 0 0 0 0 0 0 0]
|
SWAP5 用于交换第 1 个和第 6 个堆栈项。
1
| [address_deployed 0 0 0 0 0 0 0 calldatasize 0 0 0 0 0 0 0]
|
GAS 会返回仍然可用的总气体量
1
| [gas address_deployed 0 0 0 0 0 0 0 calldatasize 0 0 0 0 0 0]
|
以上基本是为 CALL 做准备,如果调用失败,则操作码将 0 压入堆栈,否则为 1。
注意:如果被调用的账户没有代码,它会返回成功为true
。
堆栈参数依次为
gas
:要发送到子上下文以执行的 gas 量。
address
:要执行的上下文的帐户。
value
:wei 中的值发送到该地址
argsOffset
:内存中的字节偏移量,以字节数表示
argsSize
:使用先前指定的偏移量从内存中复制的字节大小
retOffset
:内存中的字节偏移量(以字节为单位),用于存储子上下文的返回数据。
retSize
:要复制的字节大小(返回数据的大小)。
而后续需要使 CALL 结果等于 00
1 2 3 4
| 14 6000 PUSH1 00 16 14 EQ 17 601B PUSH1 1B 19 57 JUMPI
|
因此我们需要使合约调用失败,即 REVERT
可以再次构造一个 REVERT 的合约
1 2 3 4 5 6
| 60FD PUSH1 FD // FD 是 REVERT 6000 PUSH1 00 // 这将被用作 MSTORE8 的偏移量,在内存中存储 REVERT 53 MSTORE8 // 将从偏移量 0 开始在内存中存储 `FD` 值(来自第一个 PUSH1) 6001 PUSH1 01 // 必须返回 1 字节 6000 PUSH1 00 // 从 0 返回这些字节 F3 RETURN
|
即 0x60FD60005360016000F3
Puzzle 9
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
| ############ # Puzzle 9 # ############
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
? Enter the value to send: (0) ? Enter the calldata:
|
CALLDATASIZE
PUSH1 03
1
| [03 calldatasize 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
LT 对前两个堆栈值运行比较,以查看第一个堆栈元素是否小于第二个堆栈元素。如果计算结果为 true,则在堆栈上推送1,否则改为推送0。
由于这里我们需要得到一个为 1 的结果,所以 calldatasize > 3
随后运行以下指令
1 2 3 4 5 6 7 8
| 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
|
也就是说需要满足以下条件
1 2
| CALLDATASIZE > 3 CALLVALUE * CALLDATASIZE == 8
|
这里我输入的是
1 2
| ? Enter the value to send: 2 ? Enter the calldata: 0x00000001
|
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 25 26 27 28 29 30
| ############# # Puzzle 10 # #############
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
? Enter the value to send: (0)
|
CODESIZE
CALLVALUE
SWAP1
1
| [1b callvalue 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
GT 用来计算大于
因此需要满足 callvalue < 1b,即十进制中的27
接下来执行以下指令
1 2 3 4 5 6 7 8 9 10
| 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
|
CALLDATASIZE
PUSH2 0003
SWAP1
1
| [calldatasize 3 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
MOD 用于计算第一个堆栈元素和第二个堆栈元素的模,即 a % b
1
| [calldatasize%3 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
ISZERO,如果堆栈上的最高值为 0 ,则该指令将推送 1 到堆栈上。如果任何其他数字位于堆栈顶部,则将推送 0 到堆栈。
1
| [ISZERO(calldatasize%3) 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0]
|
CALLVALUE
PUSH1 0A
ADD 前两个值相加
然后需要跳转到 0x19 处,也就是说需要满足条件
1 2
| calldatasize % 3 == 0 0x0a + callvalue = 0x19 // 10 + 15 = 25
|
所以我输入
1 2
| ? Enter the value to send: 15 ? Enter the calldata: 0x000000
|
至此,所有 Puzzle 都完成了