EVM

EVM_Puzzle

Posted by Thomas_Xu on 2022-11-27

EVM_Puzzle


author:Thomas_Xu

EVM_puzzle是一个可以很好的EVM入门阶段的一个可以上手的题库,正好最近在研究操作码的反编译,正好巩固熟练一下。

Puzzle 1

我们只需要键入输入,使程序正常跳转到JUMPDEST即可,这道题里出现的所有操作码都只占一个字节,所以JUMPDEST应当位于8的位置上。

我们只需要键入8即可通关。

Puzzle 2

同上一题一样,我们只需要键入输入,使程序正常跳转到JUMPDEST即可。

CODESIZE 获取在当前环境中运行的代码的大小,并入栈。在此示例中,我们可以通过查看序列中有多少操作码来手动检查代码的大小。此题中出现操作码都是1个字节。

在这个谜题中,我们有10个操作码,这意味着代码的大小是10个字节。注意,EVM 使用十六进制数来表示字节码。所以这里是a

SUB 会把第一个堆栈元素减去第二个堆栈元素,并将结果推送到堆栈的顶部。

根据JUMP的条件我们这里只需要让执行JUMP时栈顶的元素为6即可。

即 0a - input = 6

所以输入4即可通关。

Puzzle 3

根据JUMP的条件我们这里只需要让执行JUMP时栈顶的元素为4即可。

CALLDATASIZE等于4即可。

这里输入0x11223344即可通关

需要注意的是CALLDATASIZE统计的是字节数,而一个字节可以储存2个16进制数。所以我们应该输入8位十六进制数

Puzzle 4

根据JUMP的条件我们这里只需要让执行JUMP时栈顶的元素为0a即可。

而此题中CODESIZE为12,XOR为二进制异或运算,我们只需要进行一次简单的计算即可

1
2
3
4
0000 1010 //JUMP执行时栈顶所需  0a

0000 1100 //12 CODESIZE
0000 0110 //input

因此,我们只需要输入0000 0110即 6 就可以通关

Puzzle5

根据EQ条件,我们只需要使value等于0x0100即可(256)

所以我们输入16,解决

Puzzle6

此题考查CALLDATALOAD,由于偏移量为0,我们只需要输入0a来使JUMP正常跳转即可

因此,输入为0x000000000000000000000000000000000000000000000000000000000000000A

Puzzle7

这个题会稍微复杂一点,我们把这些字节码分开来看。

part1

此时的栈状态为空

1
2
3
4
00      36        CALLDATASIZE
01 6000 PUSH1 00
03 80 DUP1
04 37 CALLDATACOPY

者四个操作码为第一部分,前三个字节码均为CALLDATACOPY的参数,此操作码会将当前环境中的输入数据复制到内存

三个堆栈输入分别是

  1. destOffset:将复制结果的内存中的字节偏移量。
  2. offset:要复制的调用数据中的字节偏移量。
  3. size:要复制的字节大小。

因此我们可以分析出,当part1执行完之后,堆栈里的数据应该是完整的calldata

part2

此时的栈状态为:calldata

1
2
3
4
05      36        CALLDATASIZE
06 6000 PUSH1 00
08 6000 PUSH1 00
0A F0 CREATE

这里三个操作码都为CREATE的入参,CREATE操作会为我们创建一个创建具有关联代码的新帐户

它的堆栈输入分别是:

  1. value:以Wei为单位的值发送到新帐户。
  2. offset内存中的字节偏移量(以字节为单位),新帐户的初始化代码。
  3. size:要复制的字节大小(初始化代码的大小)。

堆栈输出:

  1. address:已部署合约的地址,如果部署失败,则为 0。

有关于create创建合约地址的更多信息可以看这篇文章,写的比较明白。

当执行 theopcode 时,只有 theopcode 返回的代码才是将来调用已部署合约时将执行的“运行时代码”。字节码的另一部分只使用一次,仅用于该部分。CREATE``RETURN``constructor

因此,我们可以在里面拥有我们想要的所有代码,但我们需要确保返回的代码(运行时代码)只有 1 条指令,因此将返回 1(字节)。calldata``EXTCODESIZE

让我们看看RETURN操作码是如何工作的:它从堆栈中弹出 2 个值以将它们用作以下操作的输入:

  • 从开始读取的位置开始的内存偏移
  • 要读取和返回的内存大小(以字节为单位)

因此,无论内存中有什么,我们都只想返回 1 条 1 字节的指令。我们的目标是执行。RETURN(offset=0, size=1)

用字节码翻译是。600060005360016000F3

因此,如果我们传递拼图的调用数据,它将使用该调用数据来创建和部署一个新合约,该合约的运行时代码仅为:theopcode!600160005360016000F3``00``STOP

其实只要让RETURN为一个字节就行,也就是以60016000F3结尾的字节码

最终输入0x60ff60005360016000F3通关

Puzzle 8

这个题的操作码也比较多,我们仍然把他们拆分着看

part1

1
2
3
4
00      36        CALLDATASIZE
01 6000 PUSH1 00
03 80 DUP1
04 37 CALLDATACOPY

这一部分和上一题一样,返回栈顶的是calldata的值

part2

1
2
3
4
05      36        CALLDATASIZE
06 6000 PUSH1 00
08 6000 PUSH1 00
0A F0 CREATE

这里也是一样,创建一个合约

part3

1
2
3
4
5
6
7
8
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=ALL_THE_GAS_AVAILABLE, address=ADDRESS_FROM_CREATE, value: 0, argsOffset=0, argsSize=0, retOffset=0, retSize=0)

在这里,我们只是调用部署的合约,没有任何 calldata 参数,也没有从返回值中读取任何内容。

执行堆栈后将只有一个值。如果已还原,则为 0,否则为 1。CALL

part4

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

这里是通关条件,很显然,我们需要使Call调用的入栈值为00

因此我们只需要构造一个这样的合约:

1
2
3
4
5
6
7
8
// store in memory the REVERT opcode as the only “code” of the contract
PUSH1 01
PUSH1 00
MSTORE8
// make the constructor return the stored runtime code
PUSH1 01
PUSH1 00
RETURN

字节码为:600160005360016000F3

那么这就将成为我们calldata的传入内容


notice

true

This is copyright.

...

...

00:00
00:00