5

智能合约安全系列文章反汇编·上篇

 3 years ago
source link: http://www.iterduo.com/posts/116192
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.

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

反汇编内容

由于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)如下:

0.png

反汇编分析

我们从第一部分指令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,则会顺序执行下一条指令。具体执行过程如下:

11.png

这里我们先来分析顺序执行的内容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/在线网站查看。目前栈中布局如下:

2:0x0dbe671f
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:调用参数中的函数标识符

具体执行过程如下:

222.png

目前我们对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()函数,故目前栈中布局如下:

2:0x4df7e3d0
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指令则表示并未有函数签名匹配,从而停止执行,回滚状态。

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK