Carrot漏洞分析

说明

关键的漏洞点是在于Carrot中的transReward函数。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
contract token is ERC20  {
address internal pool = address(0);
....
function initPool(address _Pool) public onlyOwner {
require(pool == address(0));
pool = _Pool;
}

function transReward(bytes memory data) public {
pool.functionCall(data);
}
....
}

其中的pool.functionCall(data)中没有对pool进行限制,理论上就可以调用任意的pool方法。

这个漏洞也可以换一个名字,取名为滥用functionCall导致的漏洞。functionCallopenzeppelin中的address库的方法。具体的使用方法如下:

Performs a Solidity function call using a low level call. A plaincall is an unsafe replacement for a function call: use this function instead.

If target reverts with a revert reason, it is bubbled up by this function (like regular Solidity function calls).

Returns the raw returned data. To convert to the expected return value, use abi.decode.

Requirements:

  • target must be a contract.

  • calling target with data must not revert.

简单点说,functionCall底层就是封装了call()函数的调用。

漏洞分析

攻击分析

根据blocksec披露的信息info,分析攻击交易的调用栈。0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9

攻击步骤大致分为四步:

  1. 攻击合约调用目标Token(Carrot)中的漏洞方法transReward()方法
  2. 调用pool合约的0xbf699b4b方法
  3. 之后测试一个空的转账
  4. 确认第三步空的转账成功成功,成功将受害者的Token转移到目标合约

关于这几个步骤中还是有疑问,为什么需要进行第二步,第二步的作用什么。下面结合Token合约进行分析。

合约分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
contract token is ERC20  {
....
function initPool(address _Pool) public onlyOwner {
require(pool == address(0));
pool = _Pool;
}

function transReward(bytes memory data) public {
pool.functionCall(data);
}


function _beforeTransfer( address from,address to,uint256 amount) private{
if(from.isContract())
// 要求调用的方法,需要是pool的owner
if(ownership(pool).owner() == from && counter ==0){
_isExcludedFromFee[from] = true;
counter++;
}
_beforeTokenTransfer(from, to, amount);
}

function transferFrom(
address sender,
address recipient,
uint256 amount
) public virtual override returns (bool) {
// 调用_beforeTransfer,对from,to,amount进行校验
_beforeTransfer(_msgSender(),recipient,amount);

if(_isExcludedFromFee[_msgSender()]){ // 判断调用者是不是pool的owner
// 如果调用者是pool的owner,则直接转账
_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;
}
....
}

与转账有关的两个函数是:transferFrom_beforeTransfer

_beforeTransfer,就是判断msg.sender是不是pool.owner()。如果是就将_isExcludedFromFee[from] = true

1
2
3
4
5
6
7
8
9
function _beforeTransfer( address from,address to,uint256 amount) private{
if(from.isContract())
// 要求调用的方法,需要是pool的owner
if(ownership(pool).owner() == from && counter ==0){
_isExcludedFromFee[from] = true;
counter++;
}
_beforeTokenTransfer(from, to, amount);
}

transferFrom()函数内部通过_isExcludedFromFee[_msgSender()]的判断语句,会存在一个分支执行。结合_beforeTransfer()的实现,transferFrom()的逻辑就是,如果判断是pool.owner直接转账。如果不是,转账之后重新授权。

如果我们直接调用transferFrom()将受害者的Token转移到我们的账户中,最终就会在_approve(sender,_msgSender(),_allowances[sender][_msgSender()].sub(amount,"ERC20: transfer amount exceeds allowance"));失败。

所以,关键的问题就是要让我们的msg.sender成为pool.owner()

综合0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9攻击分析,

Carrot.transReward(0xbf699b4b0000000000000000000000005575406ef6b15eec1986c412b9fbe144522c45ae)

Carrot.transReward的调用就是为了改变pool.owner()

1
0x6863b549bf730863157318df4496ed111adfa64f.owner()

最后的输出结果是0x5575406ef6b15eec1986c412b9fbe144522c45ae

漏洞复现

结合我们的分析,根据0xa624660c29ee97f3f4ebd36232d8199e7c97533c9db711fa4027994aa11e01b9,我们就可以写出对应的Poc。

1

调用结果如下:

参考

Carrot_exp.sol

0x5Fd1FFAcb4e7d1bEDfd7b0dc5e6eC02E65154fbE_token

https://twitter.com/BlockSecTeam/status/1579908411235237888

文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2022/10/13/Carrot-vuln/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow