关卡链接: Ethernaut Level 18 - Magic Number
攻击类型: EVM字节码编写
难度: ⭐⭐⭐⭐⭐
你需要部署一个合约,它必须满足两个条件:
runtime bytecode
)大小不能超过10个字节。whatIsTheMeaningOfLife()
函数时,必须返回 42
。这个挑战将我们带入EVM的底层。使用Solidity编写一个返回42的函数非常简单,但编译后的字节码会远远超过10字节的限制,因为它包含了函数调度器、安全检查等大量额外代码。
1 | // 编译后字节码会很长,无法通过关卡 |
因此,我们必须手动编写EVM字节码。我们需要分别构建两部分代码:
我们的运行时代码需要做两件事:
42
(十六进制为 0x2a
) 放入内存。这需要以下操作码(Opcodes):
Opcode | 名称 | 作用 |
---|---|---|
0x60 |
PUSH1 |
将1个字节的数据压入堆栈。 |
0x52 |
MSTORE |
MSTORE(p, v) : 将值 v 存入内存地址 p 。 |
0xf3 |
RETURN |
RETURN(p, s) : 从内存地址 p 开始,返回 s 字节的数据。 |
执行步骤如下:
PUSH1 0x2a
: 将 42
压入堆栈。PUSH1 0x80
: 将内存地址 0x80
压入堆栈。(0x80
是Solidity中自由内存指针的起始位置,使用它是惯例)。MSTORE
: mstore(0x80, 0x2a)
,将 42
存入内存。PUSH1 0x20
: 将返回值大小 32
字节(一个 uint256
)压入堆栈。PUSH1 0x80
: 将返回的内存地址 0x80
压入堆栈。RETURN
: return(0x80, 0x20)
,返回结果。将这些步骤转换为字节码:602a
6080
52
6020
6080
f3
这个字节码的长度是10字节:0x602a60805260206080f3
。完美符合要求!
创建代码的任务是将上面的10字节运行时代码返回给EVM。它需要做两件事:
这需要 CODECOPY
操作码:
Opcode | 名称 | 作用 |
---|---|---|
0x39 |
CODECOPY |
CODECOPY(d, p, s) : 从代码的 p 位置开始,复制 s 字节到内存的 d 位置。 |
执行步骤如下:
0x00
处。0x00
处返回10字节的代码。字节码如下:
600a
: PUSH1 0x0a
(运行时代码长度: 10字节)600c
: PUSH1 0x0c
(运行时代码在创建代码中的起始位置: 第12字节)6000
: PUSH1 0x00
(目标内存地址: 0)39
: CODECOPY
600a
: PUSH1 0x0a
(要返回的数据长度: 10字节)6000
: PUSH1 0x00
(要返回的内存地址: 0)f3
: RETURN
创建代码为: 0x600a600c600039600a6000f3
。它的长度是12字节。
最终部署的字节码是 创建代码 + 运行时代码:0x600a600c600039600a6000f3
+ 602a60805260206080f3
最终字节码: 0x600a600c600039600a6000f3602a60805260206080f3
我们可以使用 Foundry 的内联汇编和 create
操作码来部署这段字节码。
1 | // SPDX-License-Identifier: Unlicense |
42
。create
操作码(可以通过内联汇编或发送裸交易)来部署这段字节码,得到求解器合约的地址。Ethernaut
关卡。这个关卡本身不是一个漏洞,而是一个EVM编程的练习。然而,它揭示了在进行字节码级别的审计时需要注意的事项:
create
或 create2
),需要特别审查其字节码的来源和功能。evm.codes
是一个极好的交互式参考网站。assembly
): Solidity允许在代码中直接嵌入汇编语言,提供了对EVM更底层的控制,但同时也带来了更大的风险和复杂性。create
操作码: 用于从代码中部署新合约。extcodesize
操作码: 用于获取一个地址上的代码大小。核心概念:
creation code
和 runtime code
。creation code
在部署时执行一次,其返回值是 runtime code
。runtime code
是永久存储在链上的代码,响应外部调用。攻击向量:
防御策略:
在智能合约的世界中,最简单的漏洞往往隐藏着最深刻的安全教训。 🎓