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
,然后我们把 X
和 C
连接到一起,并将 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 一致,题目完成。