智能合约的汇编解读

智能合约的汇编解读

七月 26, 2018

智能合约是一种基于栈操作的虚拟机。本文大致从汇编以及栈的角度来分析智能合约.

智能合约逆向的汇编解读


0x00 前言

智能合约是一种基于栈操作的虚拟机。本文大致从汇编以及栈的角度来分析智能合约.

0x01 工具

  1. Binary Ninja (类似于ida的分析工具)
  2. Ethersplay (Ninja上用于分析EVM的插件)

0x02 测试代码

1
2
3
4
5
6
7
8
9
10
11
12
pragma solidity ^0.4.0;

contract Test {
uint256 value;
function Test() {
value = 5;
}
function set_value(uint256 v) {
value = v;
}
function() payable {}
}

0x03 runtime bytecode

http://remix.ethereum.org/在线编译后,compile -- details -- runtime bytecode -- object可以找到我们需要的字节码。

1
608060405260043610603e5763ffffffff7c0100000000000000000000000000000000000000000000000000000000600035041663b0f2b72a81146040575b005b348015604b57600080fd5b50603e6004356000555600a165627a7a72305820b7b3c9a86752d5800eeef3e2aa4be3ac22127672dafe88e6857f442a7ec5ab940029

打开Ninja,主界面如下:

New新建一个文件,然后把字节码复制进去。

选中所有内容,右键PasteFrom -- Raw hex

可以得到如下内容:

添加EVM格式头后,用Ninja重新打开:

(Ninja用法和IDA类似,空格键切换视图)

0x04 分析

(此处stack格式为: stack = [栈底 , 栈顶])
首先看到是_dispatcher,调度器,用来处理交易数据,以及调用需要交互的函数。

先分析前几行代码:

1
2
3
4
5
6
7
8
PUSH1 #80       //向栈中压入0x80
//stack = [0x80]

PUSH1 #40 //向栈中压入0x40
//stack = [0x80 , 0x40]

MSTORE //在memory中偏移64*32处写入0x80, 即开辟2k的存储空间
//stack = []

此处,PUSH指令后面的编号代表了向栈中压入多少字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PUSH1 #4        //向栈中压入0x4
//stack = [0x4]

CALLDATASIZE //获取当前调用中的数据长度
//stack = [0x4 , size]

LT //less than: result = (stack[1] < stack[0] : 1 ? 0)
//stack = [result]

PUSH1 #3e //向栈中压入0x3e
//stack = [result , 0x3e]

JUMPI //jump if: if result == 1 : jump
//stack = []

关于此处的CALLDATASIZE判断,用来识别函数。

EVM通过函数keccak256的前4个字节识别函数。Dispatcher会检查我们发往合约的calldata大小至少为4字节。如果我们发送的数据小于四字节,程序将直接跳入_fallback,不会与其他函数发生交互。

接着看下一块代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PUSH4 #ffffffff //向栈中压入0x3e
//stack = [0xffffffff]

PUSH29 #100000000000000000000000000000000000000000000000000000000
//向栈中压入1000000000000........
//stack = [0xffffffff , 1000000.....]

PUSH1 #0 //向栈中压入0
//stack = [0xffffffff , 1000000..... , 0]

CALLDATALOAD //在发往智能合约的数据索引为0处读取32字节入栈
//stack = [0xffffffff , 1000000..... , data]

DIV //div_result = div(stack[2] , stack[1])
//stack = [0xffffffff , div_result]

AND //and_result = and(stack[1] , stack[0])
//stack = [and_result]

(这里不懂可以看一下后缀表达式)

在这里的除法部分,data为32字节,与29字节的数据做除法取整,得到的是data的前四个字节,即函数的标识符。下面的and_result用symbol代替

与运算的目的是将标识符后面的28字节清零,调整栈。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PUSH4 #b0f2b72a //向栈中压入0xb0f2b72a
//stack = [symbol , 0xb0f2b72a]

DUP2 //复制栈中的第二个数据,即stack[0],压入栈顶
//stack = [symbol , 0xb0f2b72a , symbol]

EQ //equal: result = (stack[2] == stack[1] : 1 ? 0)
//stack = [symbol , result]

PUSH1 #40 //向栈中压入0x40
//stack = [symbol , result , 0x40]

JUMPI //jump if: if result == 1 : jump
//stack = [symbol]

如果成功跳转,程序将进入0x40处。

按照文中示例,如果不能成功跳转,程序将跳入_fallback。如果程序中包含其他函数,程序会继续将symbol与其他函数进行比较,如果匹配成功将会跳转。如果所有函数都无法成功匹配,才会跳入_fallback

1
2
JUMPDEST        //作为JUMP的占位符
//stack = [symbol]

然后我们进入分析set_value函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CALLVALUE       //当前调用所带的金额,即一次交易中发送的wei的数量
//stack = [value]

DUP1 //复制栈中的第一个数据,即stack[0],压入栈顶
//stack = [value , value]

ISZERO //result = (stack[1] == 0 : 1 ? 0)
//stack = [value , result]

PUSH #4b //向栈中压入0x4b
//stack = [value , result , 0x4b]

JUMPI //jump if: if result == 1 : jump
//stack = [value]

现在就出现了问题:只有当value == 0的时候,才会跳转到左侧分支。

我们先来分析一下右侧分支:

1
2
3
4
5
6
7
8
PUSH1 #0        //向栈中压入0
//stack = [value , 0]

DUP1 //复制栈中的第一个数据,即stack[1],压入栈顶
//stack = [value , 0 , 0]

REVERT //停止当前执行,保存状态改变,返回数据 mem[p...(p+s)]
//stack = [mem[p...(p+s)]]

我们回头看一下源代码。set_value函数没有被标记为payable,所以该交易会被拒绝。

然后分析一下左侧分支。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
JUMPDEST        //作为JUMP的占位符
//stack = [value]

POP //从栈中弹出一个数据
//stack = []

PUSH1 #3e //向栈中压入0x3e
//stack = [0x3e]

PUSH1 #4 //向栈中压入0x4
//stack = [0x3e , 0x4]

CALLDATALOAD //获取数据中 mem[p...(p+s)]的数据
//stack = [0x3e , data]

PUSH1 #0 //向栈中压入0
//stack = [0x3e , data , 0]

SSTORE //将data的值存入索引为0位置的32bit
//stack = [0x3e]

JUMP //跳转到栈顶值的位置
//stack = []

程序跑到了_fallback

1
STOP

0x05参考文章

https://www.jianshu.com/p/230c6d805560

https://arvanaghi.com/blog/reversing-ethereum-smart-contracts/

0x06 后记

在跳入set_value函数的时候踩到了两个坑。

其一是栈中的symbol: 为什么跳入set_value的时候symbol不见了。想了一下发现是自己智障了,毕竟那里也是一个跳转,需要从栈中弹出一个值作为目标地址。

其二是在set_value函数中为什么会正常逻辑会跳转到REVERT: 查了下资料发现是payable的问题。

之前有说过这个Binary Ninja的问题,我只能说:支持正版!