关卡链接: 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: CODECOPY600a: 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 是永久存储在链上的代码,响应外部调用。攻击向量:
防御策略:
在智能合约的世界中,最简单的漏洞往往隐藏着最深刻的安全教训。 🎓