目录

EVM 自产生程序(quine)

目录

这是来自 paradigm CTF 的一道题,题目要求是给出一段 evm bytecode,要求这段 bytecode 在 deploy 之后被调用的输出,正好和它原始的 bytecode 一摸一样。同时题目还限制了这段 bytecode 不可以使用 CODECOPY, CALL, DELEGATECALL 等指令。

通常这类程序被称为 quine program,在 wiki 中有很多其他语言的实现,我们只需要把其他语言的思路运用到 evm opcode 中即可。

解法

首先,我们需要定义一段常量 C,然后将 C PUSH 到栈上,接下来我们需要运行一段 OPCODE,这些 OPCODE 组合起来正好等于我们刚才所定义的常量 C

C 需要做的事情就是将 PUSHN 指令保存到内存中,并将 C 自身复制两份保存在内存中,最后将内存中的内容返回。

这样我们最终返回的内容就是 PUSHN + C + C,这里的 PUSHN 表示一个 PUSH 指令,因为我们还不知道 C 的长度,所以用 PUSHN 代替。

接下来我们开始拼凑 C 的内容。

C 的指令运行之前,我们将 C PUSH 到了栈上,所以在 C 中我们首先需要将 PUSHN 这个指令也保存到内存中,我们先将它 PUSH 到栈上:

PUSH1 X

因为我们还不知道 C 的长度,这里先使用未知数 X 表示 PUSHN 指令的 OPCODE。执行完成后栈中内容应为:

0: X
1: C

接下来我们可以把栈中的内容保存到内存中,但是由于栈中只有一个字节的内容,为了更高效一些我们可以晚一些和其他内容一起来保存。

因为我们需要在内存中保存两次 C,所以需要再复制一份 C,然后我们把 XC 连接到一起,并将 XC 一起保存到内存中,这段 OPCODE 为:

PUSH1 Y
SHL                   // 将 X 向左平移 Y
DUP2                  // 复制一份 C
OR                    // 将 X 和 C 连接起来
MSIZE                 // 获取内存长度,作为保存新内容的起始地址
MSTORE                // 将 XC 保存到内存中

这里我们再次引入了一个未知数 Y,即 X 需要向左平移的距离。这段 OPCODE 执行结束后,栈中内容为:

0: C

内存中内容为

0~32:        000...0XC

接下来我们将 C 再次保存到内存中,这样我们在内存中的内容就成为了 XCC,最后我们将内存中的这段内容返回即可:

PUSH1 Z
SHL                    // 需要将 C 向左平移 Z,消除栈中填充的前缀 0
MSIZE                  // 获取内存长度,作为保存新内容的起始地址
MSTORE                 // 将 C 保存到内存中
PUSH1 A
PUSH1 B
RETURN                 // 将内存中的内容返回,offset 为 A,lens 为 B

上面的指令执行完成后,内存中内容应为:

0~32:        000...0XC
32~64:       C000...00

最终我们将内存中的 XCC 返回即可。

将上面所有内容组合起来,即为 C 所包含的 OPCODEs:

PUSH1 X
PUSH1 Y
SHL
DUP2
OR
MSIZE
MSTORE
PUSH1 Z
SHL
MSIZE
MSTORE
PUSH1 A
PUSH1 B
RETURN

将其转为 bytecode:

60 X 60 Y 1B 81 17 59 52 60 Z 1B 59 52 60 A 60 B F3

这样我们知道了 C 的长度为 19 字节。那么 X 就应该是 PUSH19 的 OPCODE,即 X=72

Y 表示我们要将 X 向左平移的距离,平移后使用 OR 将其连接到 C 的前端,因此 Y=19*8=98(bits, in hex)。

Z 表示将 C 左移,消除栈中填充的前缀 0(栈中的内容默认是右对齐的)。因此 Z=(32-19)*8=68 (bits, in hex).

A 表示内存需要返回内容的起始地址,B 表示返回内容总长度,可以算出 A=27, B=0C.

所以最后 bytecode 的内容 XCC 即为:

72(607260981B8117595260681B59526027600CF3)(607260981B8117595260681B59526027600CF3)

这里我用括号括起的两段即为 C 的内容。

我们可以使用 evmcodes playgroud 来验证这段 bytecode 运行的结果:

可以看到最后输出的结果和输入的 bytecode 一致,题目完成。