7

零时科技 | 智能合约安全系列文章反汇编·上篇

 3 years ago
source link: https://mp.weixin.qq.com/s?__biz=MzU1OTc2MzE2Mg%3D%3D&%3Bmid=2247485578&%3Bidx=1&%3Bsn=6200ec07f9098d4e705f8740d0c5406d
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

fUrEJnZ.jpg!mobile

前言

通过上一篇反编译文章的学习,我们对智能合于opcode的反编译有了基础的学习,对于初学者来说,要想熟练运用还得多加练习。本篇我们来一块学习智能合约反汇编,同样使用的是Online Solidity Decompiler在线网站,智能合约反汇编对于初学者来说,虽然 较难理解, 只要能读懂智能合约反汇编,就可以非常清晰的了 解到合约的代 码逻辑, 对审计合约和C TF智能合约都有非常大的帮 助。

反汇编内容

由于solidity智能合约的opcode经过反汇编后,指令较多,我们本篇分析简明要义,以一段简单合约代码来分析其反汇编后的指令内容。合约代码如下:

pragma solidity ^0.4.24;


contract Tee {

uint256 private c;


function a() public returns (uint256) { self(2); }

function b() public { c++; }


function self(uint n) internal returns (uint256) {

if (n <= 1) { return 1; }


return n * self(n - 1);
}
}

合约部署后生成的opcode如下:

0x6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630dbe671f14604e5780634df7e3d0146076575b600080fd5b348015605957600080fd5b506060608a565b6040518082815260200191505060405180910390f35b348015608157600080fd5b5060886098565b005b60006094600260ab565b5090565b6000808154809291906001019190505550565b600060018211151560be576001905060cd565b60c86001830360ab565b820290505b9190505600a165627a7a7230582003f585ad588850fbfba4e8d96684e2c3fa427daf013d4a0f8e78188d4d475ee80029

通过在线网站Online Solidity Decompiler反汇编后结果(runtime bytecode)如下:

FvQ7fqJ.png!mobile

具体指令可参考 Github 上的OPCODE指令集,每个指令具体含义可以参考 相关文档

反汇编分析

我们从第一部分指令label_0000开始分析

  0000    60  PUSH1 0x80
0002 60 PUSH1 0x40
0004 52 MSTORE
0005 60 PUSH1 0x04
0007 36 CALLDATASIZE
0008 10 LT
0009 60 PUSH1 0x49
000B 57 *JUMPI

push指令是将字节压入栈顶,push1-push32依次代表将1字节-32字节推压入栈顶,这里PUSH1 0x80和PUSH1 0x40表示将0x80和0x40压入栈顶,故目前栈的布局如下:

1: 0x40
0: 0x80

MSTORE指令表示从栈中依次出栈两个值arg0和arg1,并把arg1存放在内存的arg0处。目前来说栈中已无数据,这里将0x80存放在内存0x40处。

PUSH1 0x04将0x04压入栈中,CALLDATASIZE指令表示获取msg.data调用数据,目前栈的布局如下:

1: calldata
0: 0x04

LT指令表示将两个栈顶的值取出,如果先出栈的值小于后出栈的值则把1入栈,反之把0入栈。这里如果calldata调用数据小于0x04字节,就将1入栈;如果calldata调用数据大于等于0x04字节,就将0入栈。目前栈的布局为:0: 0 或0: 1。

继续分析,PUSH1 0x49指令将0x49压入栈顶,目前栈的布局为:

1:0x49
0: 0 或者 1

下面一条指令JUMPI指令表示从栈中依次出栈两个值arg0和arg1,如果arg1的值为真则跳转到arg0处,否则不跳转。如果arg1值为1,则指令会跳转到0x49处;如果arg1值为0,则会顺序执行下一条指令。具体执行过程如下:

6VJJZja.png!mobile

这里我们先来分析顺序执行的内容label_000C,指令如下:

  000C    60  PUSH1 0x00
000E 35 CALLDATALOAD
000F 7C PUSH29 0x0100000000000000000000000000000000000000000000000000000000
002D 90 SWAP1
002E 04 DIV
002F 63 PUSH4 0xffffffff
0034 16 AND
0035 80 DUP1
0036 63 PUSH4 0x0dbe671f
003B 14 EQ
003C 60 PUSH1 0x4e
003E 57 *JUMPI

目前经过上一步运算栈中布局为空,PUSH1 0x00指令将0压入栈中。CALLDATALOAD指令接受一个参数,该参数可以作为发往智能合约的calldata数据的索引,然后从该索引处再读取32字节数,由于前一个指令传入的索引值为0,所以这一步指令会弹出栈中的0,将calldata32字节压入栈中。PUSH29指令将29个字节压入栈中。目前栈的布局如下:

1:0x0100000000000000000000000000000000000000000000000000000000
0:calldata值

SWAP1指令表示将堆栈顶部元素与之后的第一个元素进行交换,也就是0x0100000000000000000000000000000000000000000000000000000000和calldata值进行交换。接下来DIV指令表示(栈中第一个元素 // 栈中第二个元素)取a//b的值,这里也就是calldata的32字节除29字节,由于除法的运算关系,这里进行除法运算后的字节为4位,估计大家也可以想到,这就是函数标识符4字节。那么目前栈的布局如下:

0:函数标识符4字节

PUSH4 指令将0xffffffff压入栈中。AND指令表示将取栈中前两个参数进行AND运算,也就是函数标识符前四位0xffffffff进行AND操作,最终得到前四位的函数标识符及后28位为空补0的数值。下一条指令DUP1表示复制当前栈中第一个值到栈顶,目前栈中布局如下:

1:调用参数中的函数标识符
0:调用参数中的函数标识符

下一个指令PUSH4指令继续将函数标识符0x0dbe671f压入栈中,这里的标识符为a()函数,函数标识符我们可以在 https://www.4byte.directory/ 在线网站查看。目前栈中布局如下:

20x0dbe671f
1:调用参数中的函数标识符
0:调用参数中的函数标识符

EQ指令表示取两个栈顶值,如果两值相等就将1入栈(也就是说a()函数标识符与调用参数中的函数标识符相等),反之将0入栈。下一步PUSH1将0x4e压入栈顶。之后JUMPI指令从栈中依次出栈两个值arg0和arg1,如果arg1的值为真则跳转到arg0处,否则不跳转。目前栈中布局如下:

2:0x4e
1:1 或 0
0:调用参数中的函数标识符

从前面三个指令可看出,EQ对函数标识符进行判断后,下一步压入0x4e是为了JUMPI进行判断并跳转。也就是说如果EQ判断a()函数标识符相等(将1入栈),JUMPI执行后就会跳转到0x4e的偏移位置;反之如果EQ判断a()函数标识符不相等(将0入栈),JUMPI执行后就会顺序执行下一条语句。目前栈中布局如下:

0:调用参数中的函数标识符

具体执行过程如下:

UZV7fmq.png!mobile

目前我们对label_0000和label_000C已进行分析,从上图来看,该流程中除了顺序执行外,label_0000处0x49,label_003F处0x76和label_000C处0x4e都有相应的跳转条件。本篇我们继续分析顺序执行部分(label_003F和label_0049)指令。首先来看第一部分label_003F:

  003F    80  DUP1
0040 63 PUSH4 0x4df7e3d0
0045 14 EQ
0046 60 PUSH1 0x76
0048 57 *JUMPI

由于目前栈中只有一条数据(0:调用参数中的函数标识符)

DUP1指令表示复制栈中第一个值到栈顶。PUSH4指令将0x4df7e3d0函数标识符压入栈顶,这里函数标识符代表b()函数,故目前栈中布局如下:

20x4df7e3d0
1:调用参数中的函数标识符
0:调用参数中的函数标识符

接下来三个指令会进行栈中值进行运算和偏移量跳转设置,EQ指令把栈顶的两个值出栈,如果0x4df7e3d0和调用参数中的函数标识符相等则把1入栈,否则把0入栈。PUSH1指令将偏移量0x76压入栈中。JUMPI指令从栈中依次出栈两个值:0x76和EQ指令判断的值(1或0),如果EQ指令判断的值为真则跳转到0x76处,否则按顺序执行不跳转。故目前栈中布局如下:

2:0x76
1:1 或 0
0:调用参数中的函数标识符

我们假设EQ指令判断的值为0,那么通过JUMPI指令条件判断后,会按照顺序继续执行下一条指令。执行后,栈中依然只有一条指令(0:调用参数中的函数标识符)。

我们继续进行顺序执行,label_0049:

  0049    5B  JUMPDEST
004A 60 PUSH1 0x00
004C 80 DUP1
004D FD *REVERT

JUMPDEST指令在该上下文中表示跳转回来,也就是label_0000处0x49的跳转。之后的两条指令PUSH1和DUP1总体意思为将0压入栈顶并复制,没有实际意义。REVERT指令则表示并未有函数签名匹配,从而停止执行,回滚状态。

总结

由于反汇编内容过多,我们分为两篇分享给大家,本篇我们对反汇编的内容进行了详细讲解,下篇我们将会继续分析并串联所有指令,梳理代码逻辑。

:point_down: 扫描关注零时科技服务号 :point_down:

:point_down: 区块链安全威胁情报实时掌握 :point_down:

2Q3A7b7.jpg!mobile

  |  

· E N D ·

m6ba6jn.jpg!mobile


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK