参考:操纵预言机+提案攻击—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
恶意合约里面来执行的提案
创建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
修改了fToken
的newCollateralFactorMantissa
。
这里可以看到日志确实被修改了
{
"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
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!