Ethernaut_WP(6-10)
1822117232998002 区块链安全 543浏览 · 2025-04-14 12:33

Ethernaut_WP(6-10)

第六关

Plain Text
复制代码
该代码定义了两个合约,一个Delegate,一个Delegation

分析代码

Plain Text
复制代码
owner等于合约调用者

Plain Text
复制代码
这个合约多了一个fallback()函数,constructor函数中多了一句delegate = Delegate(_delegatAddress)fallback函数中调用了一个低级函数delegatecall()

fallback()函数我们第一关就了解过了

fallback是一个特殊函数,在以下情况下执行:

调用了不存在的函数,或者

Ether 直接发送到合约但receive()不存在或msg.data不为空

我们这一关的重点也就是delegatecall()这个函数,目标是获得“Delegation”合约的所有权

delegatecall 是 Solidity 提供的一个低级调用(Low-level Call),允许一个合约在另一个合约的上下文中执行代码,但使用的是当前合约的存储

delegatecall 的关键点:

1 目标合约的代码在调用者的上下文中执行(继承调用合约的存储布局)。

2 msg.sender 不变,仍然是原始调用者。

3 msg.value 不变,仍然是原始交易发送的金额。

4 如果存储变量的顺序不同,可能导致存储冲突和安全漏洞

语法:

(bool success, bytes memory returnData) = targetContract.delegatecall(abi.encodeWithSignature("functionName(uint256)", 123));

targetContract 是要调用的合约地址。

abi.encodeWithSignature("functionName(uint256)", 123) 是要调用的函数签名及参数。

delegatecall 返回两个值:

successtrue 表示调用成功,false 表示失败。

returnData:被调用函数返回的数据。

攻击代码如下:

图片加载失败


成功更改owner

修复方案

限制 fallback() 只能被 owner 调用

原理

只允许 owner 调用 delegatecall,攻击者无法触发 delegatecall 来劫持 owner

使用 onlyOwner 保护 delegatecall

完整修复版 Delegation 合约

原理

只有 owner 可以调用 fallback(),攻击者无法执行 delegatecall

这样即使攻击者调用 pwn(),也不会触发 delegatecall

避免 delegatecall 代理不受信任的合约

如果不需要 delegatecall,可以直接调用 delegate.pwn(),而不是 delegatecall

✅ 原理

直接禁用 delegatecall,防止 pwn()Delegation 的存储环境下执行。

使用 OpenZeppelinOwnable

原理

使用 OpenZeppelin Ownable 合约,提供 onlyOwner 保护。

transferOwnership(newOwner)owner 可以安全更改,而不会被攻击者劫持。

第七关

呃呃呃,一个空合约。。。提示是 1、Fallback 方法 2、有时候攻击一个合约最好的方法是使用另一个合约

fallback()函数调用情况上面已经说过

emmm,我们得了解一下所有合约可以接受ETH的可能方式

在以太坊(Ethereum)智能合约中,能够接收 ETH 的方式主要有以下几种:

方式
触发条件
需要 payable
说明
receive()
仅接受 无数据 的 ETH 交易

推荐,更清晰
fallback()
带数据 的 ETH 交易或无 receive()

可用作后备方案
selfdestruct()
另一个合约 selfdestruct(this)

强制 发送 ETH,无需 payable
delegatecall
目标合约可接收 ETH

间接接收 ETH
block.coinbase
矿工奖励

特殊情况

其中的selfdestruct()很特殊,它不需要payable、receive、fallback,可以直接强制转账

selfdestruct的EVM操作码是做什么的?从Solidity 文档关于停用和自毁的部分,你将了解到:

1这是从区块链上删除合约代码的唯一方法。

2储存在合约地址的剩余以太币被发送给指定的目标接收者

3存储和代码(在发送以太币后)会从状态中删除

有两个重要的注意事项也要记住:

1 即使一个合约被selfdestruct删除,它仍然是区块链历史的一部分,可能被大多数以太坊节点保留。因此,使用selfdestruct与从硬盘上删除数据不同。

2 即使合约的代码不包含对selfdestruct的调用,它仍然可以使用delegatecallcallcode执行该操作。

那么我们的目标就很明确了

使用selfdestruct()函数来强制向合约发送以太币,那么这关也就迎刃而解了

图片加载失败


成功转账

第八关

描述:打开 vault 来通过这一关!

要是想打开,那就是得知道password这个私密变量,那么我们怎么去知道呢

要解决这个问题,只要知道Solidity的存储区域的结构就很容易了。

Solidity一共有四个存储区域。

贮存

记忆

呼叫数据



在Storage中,值和变量存储在一起,因此即使是私有的,也可以被访问。这是因为,虽然变量不能从外部直接访问,但是通过访问存储空间中的值就可以进行访问。

合约的状态变量以紧凑的方式存储在存储中,以至于多个值有时使用相同的存储槽。 除了动态大小的数组和 映射mapping (见下文)外,数据是连续存储的,逐项存储,从第一个状态变量开始,该变量存储在槽 0 中。 对于每个变量,根据其类型确定以字节为单位的大小。 多个连续的项如果少于 32 字节,则尽可能打包到一个存储槽中,遵循以下规则:

存储槽中的第一项是低位对齐存储的。

值类型仅使用存储它们所需的字节数。

如果存储槽中的剩余空间不足以储存一个值类型,那么它会存储在下一个存储槽中。

结构体和数组数据总是会开启一个新槽,并且它们的数据根据这些规则紧密打包。

紧随结构体或数组数据的数据总是开始一个新的存储槽。

对于使用继承的合约,状态变量的顺序由从最基础合约开始的 C3 线性化顺序决定。 如果上述规则允许,来自不同合约的状态变量可以共享同一个存储槽。

结构体和数组的元素是依次存储的,就像它们单独声明时一样。

在 Solidity 中,状态变量按顺序存储在存储槽(storage slot)中。由于 passwordprivate 类型,它仍然存储在链上,并且可以通过区块链工具或 RPC 读取。

确定 password 变量的存储槽:

在 Solidity 中,状态变量按声明顺序分配存储槽。

bool public locked; 是第一个状态变量,占用 slot 0

bytes32 private password; 是第二个状态变量,占用 slot 1



可以用javascript来进行交互获得password

web3.eth.getStorageAt(contract.address, 1, function(x, y) {alert(web3.toAscii(y))})

修复方案

存储哈希值而不是明文


passwordHash 在链上可见,但原始 password 不可见,除非暴力破解哈希值(成本极高)。

这样即使有人读取 storage slot,他们也得知道密码本身才能调用 unlock()

commit-reveal 方案

让用户自己提交密码的哈希值,然后再提交密码本身进行验证


commitments 仅存储哈希,避免密码泄露。

用户自己管理密码哈希,不存储任何敏感信息。

存储密码 Off-Chain(链下)

让服务器或 IPFS 存储密码,而不是链上

任何敏感数据都不会直接存储在区块链上,无法通过 eth_getStorageAt 读取。

第九关

逻辑很简单,就是我们成为king之后,直接不接受后面人的钱不就行了(虽然仍然可以使用selfdestruct强制交易,但这不是这一关重点)

直接上代码

第十关

这关就是重头戏——重入漏洞分析一下合约代码

mapping(address => uint256) public balances;

做了一个捐款账单映射

记录账单映射

查询捐款数值

关键代码,取出自己的那份钱,这里的逻辑是先交易后记录,会引发重入漏洞

攻击代码

攻击流程如下

图片加载失败


需要注意的是,fallback 函数需要限制重入的次数,否则会因为无限地循环调用,导致 gas 不足。我在攻击的过程中并没有一次就全部拿走,也许是因为这个原因。

修复

为了避免重入,你可以使用检查-生效-交互模式,如下所示:

open in Remix

检查-生效-交互模式确保合约中的所有代码路径在修改合约状态之前完成对提供参数的所有必要检查(检查);只有在此之后才对状态进行任何更改(效果);它可以在所有计划的状态更改已写入存储后(交互)调用其他合约中的函数。这是一种常见的防止重入攻击的万无一失的方法,其中外部调用的恶意合约可以重复消费一个配额,重复提取一个余额等,通过使用逻辑在原始合约完成其交易之前回调。

请注意,重入不仅是以太转移的结果,也是对另一个合约的任何函数调用的结果。 此外,你还必须考虑多合约情况。被调用的合约可能会修改你依赖的另一个合约的状态。

说白了就是先记录后交易

shares[msg.sender] = 0;这一步是关键的防重入措施:在发送 ETH 之前,先把余额归零。这叫做 “Checks-Effects-Interactions” 模式,是安全合约的推荐写法。如果你先转账、后归零,那攻击者可以在 transfer() 回调时重入 withdraw() 再提一次钱(重入攻击)。

0 条评论
某人
表情
可输入 255

没有评论