Ethernaut
Hello Ethernaut
设置扩展程序的MetaMask,连接自己的MetaMask
打开控制台输入player即可看到玩家的地址
**help()**可以查看控制台还有什么功能
| (index) | Value |
|---|---|
| player | ‘current player address’ |
| ethernaut | ‘main game contract’ |
| level | ‘current level contract address’ |
| contract | ‘current level contract instance (if created)’ |
| instance | ‘current level instance contract address (if created)’ |
| version | ‘current game version’ |
| getBalance(address) | ‘gets balance of address in ether’ |
| getBlockNumber() | ‘gets current network block number’ |
| sendTransaction({options}) | ‘send transaction util’ |
| getNetworkId() | ‘get ethereum network id’ |
| toWei(ether) | ‘convert ether units to wei’ |
| fromWei(wei) | ‘convert wei units to ether’ |
| deployAllContracts() | ‘Deploy all the remaining contracts on the current network.’ |
| https://sepolia-faucet.pk910.de/ | |
| 我是在上述水龙头网址输入自己的address,然后领取一些SepETH供开启新实例,不然没办法打开实例 |
Fallback
合约如下:
1 | // SPDX-License-Identifier: MIT |
通过这关你需要
- 获得这个合约的所有权
- 把他的余额减到0
这可能有帮助
- 如何通过与ABI互动发送ether
- 如何在ABI之外发送ether
- 转换 wei/ether 单位 (参见
help()命令) - Fallback 方法
OK,现在来分析这个合约
关键函数
- constructor() - 部署时设置部署者为owner,并给予1000 ether的贡献值
- contribute() - 接受小于0.001 ether的捐款
- 如果捐款者累计贡献超过owner,可以成为新owner
- withdraw() - 只有owner可以提取合约内所有ETH
- receive() - 特殊的回退函数,当合约收到ETH时触发
- 要求:
msg.value > 0且contributions[msg.sender] > 0 - 条件满足时,调用者直接成为owner
- 要求:
所以让我们先调用 contribute() 发送少量 ETH(小于 0.001 ETH).
1 | await contract.contribute.sendTransaction({ value: 1 }) |
向合约捐赠 1 wei(小于 0.001 ETH),满足 contribute() 的入金限制。
然后在MetaMask上确认
1 | await contract.sendTransaction({ value: 1 }) |
直接向合约地址转账 1 wei(任意正数均可).
这会触发合约的 receive() 函数,检查到 msg.value > 0 且 contributions[msg.sender] > 0,于是将 owner 设置为你的地址.
等待交易确认后,可通过 await contract.owner() 验证,返回了我的地址,所以现在我就是owner
然后调用withdraw()把钱取走
1 | await contract.withdraw() |
然后提交实例,他会提示我通过了!!
![[Pasted image 20260215163402.png]]
fallback / receive 触发机制
| 场景 | 会触发哪个函数 |
|---|---|
| 调用不存在函数,且没有 receive() | fallback() |
| 向合约地址发送 ETH(无 calldata) | receive() |
| 调用不存在函数,但有 calldata(不为空) | fallback() |
| 调用存在的普通函数 | 正常函数 |
| ai对两者解释如下: |
fallback 函数
是一个特殊的、未命名的外部函数,每个合约最多可以包含一个。它主要在两种情况下被触发:
- 合约收到纯 ETH 转账(没有
msg.data) - 调用的函数不存在于合约中
从 Solidity 0.6.x 开始,fallback 功能被拆分为两个独立的特殊函数:
receive() external payable:专门处理纯 ETH 转账。fallback() external payable:处理所有其他情况(函数不存在或msg.data非空)。
receive() 函数
- 必须标记为
external和payable。 - 不能有参数,不能返回值。
- 一个合约最多只能定义一个
receive函数。
触发条件
当向合约发送 ETH 且交易数据为空(msg.data.length == 0)时,receive会被调用。例如: - 通过
send或transfer发送 ETH(gas 固定 2300)。 - 直接通过钱包向合约地址转账。
- 调用
address.call{value: x}("")(空 calldata)。
Fallout
1 |
|
可以看到Fallout函数
漏洞在于构造函数名称拼写错误(Fal1out 而不是 Fallout)
合约名是Fallout,但构造函数名是Fal1out(注意数字1代替了字母l)。
这里写了一个与合约名不同的函数Fal1out,实际上是普通的public payable函数,任何人都可以调用。所以,把自己设为owner即可!
1 |
|
查看合约当前的owner是谁
发现是零地址:0x0000000000000000000000000000000000000000
然后调用写错名字的那个Fal1out函数,这个函数虽然被注释为构造函数,但实际上是一个任何人都可以调用的公开函数,因为名字写错了!
因为是payable 类型,所以不需要发送任何以太币就可以把我设置为owner
1 |
|
如果想把资金提取出来可以:
1 |
|
虽然里面没有资金
Coin Flip
核心是:利用区块哈希可预测性来伪造连续 10 次猜中
1 |
|
题目描述:
1 |
|
核心是:利用区块哈希可预测性来伪造连续 10 次猜中
1 |
|
记录玩家 连续猜对次数。
通关条件:consecutiveWins == 10
1 |
|
记录 上一次使用的 blockhash。
1 |
|
revert() 的作用是:回滚当前交易并恢复所有状态,同时返回错误。
FACTOR被赋予了一个很大的数值,之后查看了一下发现是$2^{255}$
拿blockValue/FACTR,前面也提到FACTOR实际是等于2^255,若换成256的二进制就是最左位是0,右边全是1,而我们的blockValue则是256位的,因为solidity里“/”运算会取整,所以coinflip的值其实就取决于blockValue最高位的值是1还是0,换句话说就是跟它的最高位相等
Exploit:
在Remix中部署
1 |
|
攻击十次即可
Telephone
1 | // SPDX-License-Identifier: MIT |
可以看到
1 | function changeOwner(address _owner) public { |
msg.sender
当前函数 直接调用者
tx.origin
整个交易 最初发起者
让你调用合约之后保证msg.sender = tx.origin
但是这里可以通过中间合约调用让msg.sender != tx.origin
攻击合约:
1 | // SPDX-License-Identifier: MIT |
在Remix中deploy一下,attack 实例的地址
就成功了!
Token
1 | // SPDX-License-Identifier: MIT |
这关主要考察的是整数下溢 即 Underflow
在 transfer 中通过 balances[msg.sender] - _value >= 0 判断余额是否充足,但 uint256 永远 ≥0,导致可通过发送大于余额的 token 触发整数下溢,使余额回绕为 2^256-1,从而获得大量 token。
为什么uint256会永远≥0:
Solidity 0.6.0 不会自动检查整数溢出,所以可以通过构造转账让余额变成一个超大的数。
什么是整数下溢:
整数下溢(Underflow) 指:
当一个整数 减去一个比自己大的数 时,结果低于该类型最小值,从而 发生回绕(wrap around)。
就是uint256 的取值范围是:0 <= uint256 <= 2^256 - 1
如果计算 $0 - 1 = -1$ 但是uint256不允许负数,所以执行了$$0 - 1 = 2^{256} - 1$$这就是回绕
关键代码:
1 | require(balances[msg.sender] - _value >= 0); |
所以只需要向任意地址发送21个token,然后自己就变成了$2^{256} - 1$
1 | await contract.transfer("0x0000000000000000000000000000000000000001", 21) |
Delegation
1 | // SPDX-License-Identifier: MIT |
1 | fallback() external { |
任何不存在的函数调用 都会进入 fallback 然后 delegatecall 到 Delegate 合约
这里解释一下fallback的作用:
fallback() 是一种 兜底函数。
当我调用了 不存在的函数
例如:
1 | contract A { |
如果你调用:
1 | A.test() |
因为 test() 不存在,那么fallback() 就被触发
所以在这道题中:
如果有人调用不存在的函数,那么fallback触发,就把调用数据转发给 delegate 合约
查了一下delegatecall的作用:
delegatecall 是 EVM 的一种 低级调用方式。
普通调用:
1 | A -> call -> B |
执行环境:
1 | 代码:B |
而 delegatecall:
1 | A -> delegatecall -> B |
执行环境:
1 | 代码:B |
总结:
1 | delegatecall = 用别人的代码 改自己的变量 |
当执行:
1 | delegatecall(delegate) |
执行的是:
Delegate 的代码
但是修改的是:
1 | Delegation 的 storage |
例如 Delegate 代码:
1 | owner = msg.sender; |
正常情况下:
1 | Delegate.owner = msg.sender |
但是 delegatecall 时:
1 | slot0 = msg.sender |
而 slot0 在 Delegation 里是:
1 | Delegation.owner |
所以结果变成:
1 | Delegation.owner = msg.sender |
这就是 权限接管漏洞。
言归正传:
输入:
1 | keccak256("pwn()") |
因为直接调用pwn(),发送到链上的数据不是字符串,而是function selector + 参数
selector = keccak256(“函数名(参数)”) 取前 4字节 即 0xdd365b8b
所以调用:pwn() 实际上发送的是:0xdd365b8b
但是这里我输入keccak256(“pwn()”) 是因为浏览器控制台没有 keccak256 这个函数
所以在Remix 中输入web3.utils.keccak256("pwn()")去查一下
0xdd365b8b15d5d78ec041b851b68c8b985bee78bee0b87c4acf261024d8beabab
取前4字节即可
然后执行:
1 | await contract.sendTransaction({ |
即可通关
这里看到一个存储碰撞
Storage Collision(存储碰撞)
Solidity 的变量 不是按变量名存储,而是按 slot(槽)顺序存储。
例如:
1 | contract A { |
storage 布局:
1 | slot0 -> owner |
现在另一个合约:
1 | contract B { |
storage:
1 | slot0 -> number |
当 A 使用:
1 | delegatecall(B) |
执行环境变成:
1 | 代码: B |
也就是说:
B 代码写的是:
1 | admin = msg.sender; |
但是写入的是 : slot1
而 A 的 slot1 是:value, 所以A.value 被改
这就是 storage collision。
Force
题目描述
1 | 有些合约就是拒绝你的付款,就是这么任性 `¯\_(ツ)_/¯` |
1 | // SPDX-License-Identifier: MIT |
合约“拒收 ETH” ≠ 无法被强制转账
fallback / receive 并不是必须执行的,可以通过绕过fallback
Solidity 有个非常“霸道”的机制:
1 | selfdestruct(address payable to) |
它的作用是:
销毁当前合约,把余额强制发送给目标地址,而且不会调用目标合约的 fallback / receive
这一关的目标是使合约的余额大于0
但是它没有payable/receive/fallback函数,根本正常打钱不了
所以我们可以写一个合约自爆给force送钱
1 | // SPDX-License-Identifier: MIT |





