(复现)操纵预言机+提案攻击—Fortress Loans

参考:操纵预言机+提案攻击—Fortress Loans被黑事件分析 黑客操作分析,提案操作,恶意合约(5月8日)

参考:操纵预言机+提案攻击—Fortress Loans被黑事件分析 | 登链社区 | 深入浅出区块链技术 (learnblockchain.cn)

说在前面:有对漏洞复现研究的小伙伴可以私信我,一起交流

黑客操作分析

黑客地址 https://bscscan.com/address/0xa6af2872176320015f8ddb2ba013b38cb35d22ad

攻击流程

创建提案(提案内容为设置fToken的抵押因子为700000000000000000)->给提案投票->使提案通过->创建exp合约->exp合约中执行提案->修改ftoken质押因子成功->exp调用了Chain合约的submit函数修改了其中的状态变量->修改了状态变量fcds最终修改了价格预言机中的价格。->套利离场

提案操作

创建提案时间为5月3日

https://bscscan.com/tx/0x18dc1cafb1ca20989168f6b8a087f3cfe3356d9a1edd8f9d34b3809985203501

提案合约0x0db3b68c482b04c49cd64728ad5d6d9a7b8e43e6

对提案的操作根据的日志分析

https://bscscan.com/address/0xe79ecdb7fedd413e697f083982bac29e93d86b2e#events

1.ProposalCreated 提案(攻击者为11号提案)

https://bscscan.com/tx/0x12bea43496f35e7d92fb91bf2807b1c95fcc6fedb062d66678c0b5cfe07cc002#eventlog

2.调用了两次VoteCast

给提案投票(投了119774334170940063343039) 都是( 5月6日)

https://bscscan.com/tx/0x83a4f8f52b8f9e6ff1dd76546a772475824d9aa5b953808dbc34d1f39250f29d

给提案投票,投了296193548055351919633063

https://bscscan.com/tx/0xc368afb2afc499e7ebb575ba3e717497385ef962b1f1922561bcb13f85336252#eventlog

3.ProposalQueued 将提案插入队列中(5月6日)

https://bscscan.com/tx/0x647c6e89cd1239381dd49a43ca2f29a9fdeb6401d4e268aff1c18b86a7e932a0

调用queue函数

function queue(uint proposalId) public {
        require(state(proposalId) == ProposalState.Succeeded, "GovernorAlpha::queue: proposal can only be queued if it is succeeded");
        Proposal storage proposal = proposals[proposalId];
        uint eta = add256(block.timestamp, timelock.delay());
        for (uint i = 0; i < proposal.targets.length; i++) {
            _queueOrRevert(proposal.targets[i], proposal.values[i], proposal.signatures[i], proposal.calldatas[i], eta);
        }
        proposal.eta = eta;
        emit ProposalQueued(proposalId, eta);
    }

走到第一步,state函数中的quorumVotes

....
} else if (proposal.forVotes <= proposal.againstVotes || proposal.forVotes < quorumVotes()) {
            return ProposalState.Defeated;
....

quorumVotes返回400000个FTS

function quorumVotes() public pure returns (uint) { return 400000e18; } // 400,000 = 4% of FTS

而这个proposal.forVotes是在前面Votecast中修改的

function _castVote(address voter, uint proposalId, bool support) internal {
        require(state(proposalId) == ProposalState.Active, "GovernorAlpha::_castVote: voting is closed");
        Proposal storage proposal = proposals[proposalId];
        Receipt storage receipt = proposal.receipts[voter];
        require(receipt.hasVoted == false, "GovernorAlpha::_castVote: voter already voted");
        uint96 votes = fts.getPriorVotes(voter, proposal.startBlock);

        if (support) {
            proposal.forVotes = add256(proposal.forVotes, votes);
        } else {
            proposal.againstVotes = add256(proposal.againstVotes, votes);
        }

        receipt.hasVoted = true;
        receipt.support = support;
        receipt.votes = votes;

        emit VoteCast(voter, proposalId, support, votes);
    }

由于两次投票,可以满足这个条件

4.ProposalExecuted 执行提案(5月8日)

https://bscscan.com/tx/0x13d19809b19ac512da6d110764caee75e2157ea62cb70937c8d9471afcb061bf

恶意合约里面来执行的提案

恶意合约(5月8日)

创建exp tx

https://bscscan.com/tx/0x4800928c95db2fc877f8ba3e5a41e208231dc97812b0174e75e26cca38af5039

exp合约

https://bscscan.com/address/0xcd337b920678cf35143322ab31ab8977c3463a45

攻击前黑客给exp转了一些钱(100FTS 3.020309536199074866MAHA)

0x6a04f47f839d6db81ba06b17b5abbc8b250b4c62e81f4a64aa6b04c0568dc501  Transfer    17634604    4 days 16 hrs ago   Fotress Protocol Exploiter  OUT  MahaDAO: MAHA Token    0 BNB   0.00020393
0xd127c438bdac59e448810b812ffc8910bbefc3ebf280817bd2ed1e57705588a0  Transfer    17634599    4 days 16 hrs ago   Fotress Protocol Exploiter  OUT  Fortress Protocol: FTS Token   0 BNB   0.00045206

攻击交易

https://bscscan.com/tx/0x13d19809b19ac512da6d110764caee75e2157ea62cb70937c8d9471afcb061bf

首先是调用_setCollateralFactor修改了fTokennewCollateralFactorMantissa

这里可以看到日志确实被修改了

{
  "fToken": "0x854c266b06445794fa543b1d8f6137c35924c9eb",
  "oldCollateralFactorMantissa": "0",
  "newCollateralFactorMantissa": "700000000000000000"
}

重点是submit函数,之所以能够成功修改状态变量fcds,是因为submit函数中缺少了对signer本身的校验以及power的校验。

用一下SharkTeam的图

function submit(
    uint32 _dataTimestamp,
    bytes32 _root,
    bytes32[] memory _keys,
    uint256[] memory _values,
    uint8[] memory _v,
    bytes32[] memory _r,
    bytes32[] memory _s
  ) public { // it could be external, but for external we got stack too deep
    uint32 lastBlockId = getLatestBlockId();
    uint32 dataTimestamp = squashedRoots[lastBlockId].extractTimestamp();

    require(dataTimestamp + padding < block.timestamp, "do not spam");
    require(dataTimestamp < _dataTimestamp, "can NOT submit older data");
    // we can't expect minter will have exactly the same timestamp
    // but for sure we can demand not to be off by a lot, that's why +3sec
    // temporary remove this condition, because recently on ropsten we see cases when minter/node
    // can be even 100sec behind
    // require(_dataTimestamp <= block.timestamp + 3,
    //   string(abi.encodePacked("oh, so you can predict the future:", _dataTimestamp - block.timestamp + 48)));
    require(_keys.length == _values.length, "numbers of keys and values not the same");

    bytes memory testimony = abi.encodePacked(_dataTimestamp, _root);

    for (uint256 i = 0; i < _keys.length; i++) {
      require(uint224(_values[i]) == _values[i], "FCD overflow");
      fcds[_keys[i]] = FirstClassData(uint224(_values[i]), _dataTimestamp);
      testimony = abi.encodePacked(testimony, _keys[i], _values[i]);
    }

    bytes32 affidavit = keccak256(testimony);
    uint256 power = 0;

    uint256 staked = stakingBank.totalSupply();
    address prevSigner = address(0x0);

    uint256 i = 0;

    for (; i < _v.length; i++) {
      address signer = recoverSigner(affidavit, _v[i], _r[i], _s[i]);
      uint256 balance = stakingBank.balanceOf(signer);

      require(prevSigner < signer, "validator included more than once");
      prevSigner = signer;
      if (balance == 0) continue;

      emit LogVoter(lastBlockId + 1, signer, balance);
      power += balance; // no need for safe math, if we overflow then we will not have enough power
    }

    require(i >= requiredSignatures, "not enough signatures");
    // we turn on power once we have proper DPoS
    // require(power * 100 / staked >= 66, "not enough power was gathered");

    squashedRoots[lastBlockId + 1] = _root.makeSquashedRoot(_dataTimestamp);
    blocksCount++;

    emit LogMint(msg.sender, lastBlockId + 1, staked, power);
  }

修改了预言机中的价格,导致后续获取价格的时候出错了(这里好像SharkTeam弄错了,这里调用的是getCurrentValue而不是getCurrentValues)

function getCurrentValue(bytes32 _key) external view returns (uint256 value, uint256 timestamp) {
    FirstClassData storage numericFCD = fcds[_key];
    return (uint256(numericFCD.value), numericFCD.dataTimestamp);
  }

交易细节查看Exploit中的步骤。主要为套利

EXP:https://github.com/8olidity/DeFiVulhub/tree/main/Fortress

点赞 2
收藏 3
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

1 条评论

请先 登录 后评论
求职(安全岗)
求职(安全岗)
江湖只有他的大名,没有他的介绍。