函数代码相似性检验之Simhash的应用

admin 2022年1月1日17:01:17评论67 views字数 15347阅读51分9秒阅读模式

引言

在二进制代码识别的过程中,我们常用的方法是特征码识别和相似性检验来判断目标代码是否属于某个类别的代码(比如说识别正常代码还是恶意代码).本文我们来介绍Simhash 的计算与检验原理,与如何应用到对区块链的合约进行识别分类.

Simhash 的原理

我们常见的哈希算法:MD5 ,Sha-256 ,keccak 等算法是把一段文字文本单向序列化为一串十六进制的字符串.在密码学里面,我们可以理解文本处理算法是单向或者双向的,单向的意思是文本经过特定的处理之后不能够恢复成为原文;双向的意思是文本经过算法的处理可以由特定的规则或者密钥把处理过的文本恢复成原文.上面提到的哈希算法还存在一个特性就是雪崩效应,意思是原本任意修改了某一处字符,都会导致哈希之后的结果“完全不相同”,举个例子:

  1. >>> import hashlib

  2. >>> md5 = hashlib.md5()

  3. >>> md5.update('mnhjkiuy6789')

  4. >>> md5.hexdigest()

  5. '2f75db558233d9a37f96820887ec4d00'

  6. >>> md5 = hashlib.md5()

  7. >>> md5.update('mnhjkiUy6789')

  8. >>> md5.hexdigest()

  9. 'f2feb3213f33cba03907cfda315e47fd'

Simhash 则是和MD5 ,Sha-256 等算法反过来,它不考虑雪崩效应对结果的影响,我们修改文本其中一些地方,计算出来的结果哈希值有改变,存在部分相同,举个例子:

  1. >>> from simhash import Simhash

  2. >>> hex(Simhash('mnhjkiuy6789').value)

  3. '0x5d680d86c2b80b89'

  4. >>> hex(Simhash('mnhjkioy6789').value)

  5. '0x5b4899a643b90fb1'

  6. >>> hex(Simhash('mnhjkiuy679').value)

  7. '0x58400c8280b84181'

那么要计算两个文本之间是否相似,我们可以使用distance() 函数来计算相似结果.

  1. >>> Simhash('mnhjkiuy6789').distance(Simhash('mnhjkioy6789'))

  2. 14

智能合约代码结构分析

智能合约的代码从整体上可以分为两部分:入口处理代码和函数执行代码;从函数上可以分为四部分:预检测代码,参数处理代码,返回值处理代码和主函数代码.下面用一段简单的示例代码来分析:

  1. pragma solidity 0.4.25;

  2. contract test {

  3.    function a() {

  4.        uint d = 123;

  5.    }

  6.    function b() {

  7.        uint c = block.number;

  8.    }

  9. }

对应的EVM 汇编:

  1. ; ---- 合约Constructor 函数代码 ---

  2. 000 PUSH1 80

  3. 002 PUSH1 40

  4. 004 MSTORE

  5. 005 CALLVALUE

  6. 006 DUP1

  7. 007 ISZERO

  8. 008 PUSH2 0010

  9. 011 JUMPI

  10. 012 PUSH1 00

  11. 014 DUP1

  12. 015 REVERT

  13. 016 JUMPDEST

  14. 017 POP

  15. 018 PUSH1 df

  16. 020 DUP1

  17. 021 PUSH2 001f

  18. 024 PUSH1 00

  19. 026 CODECOPY

  20. 027 PUSH1 00

  21. 029 RETURN

  22. 030 STOP

  23. ; ---- 合约执行代码 ----

  24. ; ---- 合约代码第一部分:从CALLDATALOAD 中获取用户要调用的函数地址并跳转 ----

  25. 031 PUSH1 80

  26. 033 PUSH1 40

  27. 035 MSTORE

  28. 036 PUSH1 04

  29. 038 CALLDATASIZE

  30. 039 LT

  31. 040 PUSH1 49

  32. 042 JUMPI

  33. 043 PUSH1 00

  34. 045 CALLDATALOAD  ;  CALLDATALOAD 中获取函数hash

  35. 046 PUSH29 0100000000000000000000000000000000000000000000000000000000

  36. 076 SWAP1

  37. 077 DIV

  38. 078 PUSH4 ffffffff

  39. 083 AND

  40. 084 DUP1

  41. ; ---- 把从CALLDATALOAD 中获取的函数哈希来进行比对并跳转 ----

  42. 085 PUSH4 0dbe671f

  43. 090 EQ

  44. 091 PUSH1 4e

  45. 093 JUMPI

  46. 094 DUP1

  47. ; ---- 等价于代码 if (CALLDATALOAD[ : 4] == '0dbe671f') JUMP 0x4E;

  48. 095 PUSH4 cd580ff3

  49. 100 EQ

  50. 101 PUSH1 62

  51. 103 JUMPI

  52. ; ---- 等价于代码 if (CALLDATALOAD[ : 4] == 'cd580ff3') JUMP 0x62;

  53. 104 JUMPDEST

  54. 105 PUSH1 00

  55. 107 DUP1

  56. 108 REVERT  ; 执行合约异常退出并回滚数据

  57. ; ---- 等价于代码 else revert();

  58. ; ---- 合约中各函数的实现代码 ----

  59. ; ---- 函数"0dbe671f" 入口点代码

  60. 109 JUMPDEST

  61. 110 CALLVALUE

  62. 111 DUP1

  63. 112 ISZERO

  64. 113 PUSH1 59

  65. 115 JUMPI

  66. 116 PUSH1 00

  67. 118 DUP1

  68. 119 REVERT

  69. ; ---- 函数预检测代码,意思是对payable 进行检测,如果该函数不支持接收ETH Value ,那就退出执行

  70. 120 JUMPDEST

  71. 121 POP

  72. 122 PUSH1 60

  73. 124 PUSH1 a0

  74. 126 JUMP

  75. ; ---- 函数参数获取代码,因为这里没有参数,所以可以看到PUSH + (PUSH + JUMP) 的代码.

  76. ; ---- PUSH + JUMP 指的是跳转到0x76 这个位置(等价于JUMP 0x76),这个位置是函数的主入口点

  77. ; ---- PUSH1 60 指向的是返回值处理的代码,在执行完成函数代码之后就会跳转到这里

  78. 127 JUMPDEST

  79. 128 STOP

  80. ; ---- 函数返回值处理代码

  81. 129 JUMPDEST

  82. 130 CALLVALUE

  83. 131 DUP1

  84. 132 ISZERO

  85. 133 PUSH1 6d

  86. 135 JUMPI

  87. 136 PUSH1 00

  88. 138 DUP1

  89. 139 REVERT

  90. ; ---- 函数"cd580ff3" 的预处理代码

  91. 140 JUMPDEST

  92. 141 POP

  93. 142 PUSH1 8a

  94. 144 PUSH1 04

  95. 146 DUP1

  96. 147 CALLDATASIZE

  97. 148 SUB

  98. 149 DUP2

  99. 150 ADD

  100. 151 SWAP1

  101. 152 DUP1

  102. 153 DUP1

  103. 154 CALLDATALOAD

  104. 155 SWAP1

  105. 156 PUSH1 20

  106. 158 ADD

  107. 159 SWAP1

  108. 160 SWAP3

  109. 161 SWAP2

  110. 162 SWAP1

  111. 163 POP

  112. 164 POP

  113. 165 POP

  114. 166 PUSH1 a9

  115. 168 JUMP

  116. ; ---- 函数"cd580ff3" 的参数获取代码

  117. 169 JUMPDEST

  118. 170 PUSH1 40

  119. 172 MLOAD

  120. 173 DUP1

  121. 174 DUP3

  122. 175 DUP2

  123. 176 MSTORE

  124. 177 PUSH1 20

  125. 179 ADD

  126. 180 SWAP2

  127. 181 POP

  128. 182 POP

  129. 183 PUSH1 40

  130. 185 MLOAD

  131. 186 DUP1

  132. 187 SWAP2

  133. 188 SUB

  134. 189 SWAP1

  135. 190 RETURN

  136. ; ---- 函数"cd580ff3" 的返回值处理代码

  137. 191 JUMPDEST

  138. 192 PUSH1 00

  139. 194 PUSH1 7b

  140. 196 SWAP1

  141. 197 POP

  142. 198 POP

  143. 199 JUMP

  144. ; ---- 函数"0dbe671f" 的主体代码

  145. 200 JUMPDEST

  146. 201 PUSH1 00

  147. 203 DUP2

  148. 204 SWAP1

  149. 205 POP

  150. 206 SWAP2

  151. 207 SWAP1

  152. 208 POP

  153. 209 JUMP

  154. ; ---- 函数"cd580ff3" 的主体代码

  155. 210 STOP

  156. ; ---- 代码结束

  157. 211 LOG1

  158. 212 PUSH6 627a7a723058

  159. 219 SHA3

  160. 220 INVALID

  161. 221 INVALID

  162. 222 DUP8

  163. 223 PUSH19 42af5e74d82539058271e2d642452ff9c64837

  164. 243 INVALID

  165. 244 PUSH26 597224469f99f60029

  166. ; ---- 这些是CBOR 元数据,不是合约的代码

至此,我们已经了解到几点:

  1. 编译好的solidity 汇编中包含了Constructor 代码和合约执行的代码,Constructor 代码是合约部署的时候由EVM 执行的初始化代码.初始化完成之后,后续要调用合约里的函数代码都在合约执行代码里面

  2. 合约执行代码里面整体分为两部分,函数派发处理代码和函数代码

  3. 对于函数代码来说分为四部分:预检测代码,参数处理代码,返回值处理代码和函数主体代码

理解这些细节之后,接下来就是尝试从合约的EVM 汇编中抽离函数代码出来.

定位合约执行代码

分析合约代码的第一步首先是定位到合约执行代码的部分.一般来说,这部分的代码都是先判断CALLDATASIZE 是否小于4 .根据这一点,提炼出来两种字节码识别方式.

  1. PUSH1

  2. PUSH1

  3. MSTORE

  4. PUSH1

  5. CALLDATASIZE

  6. ----

  7. PUSH1

  8. PUSH1

  9. MSTORE

  10. CALLDATASIZE

示例合约汇编代码:

  1. ; ---- 合约执行代码 ----

  2. ; ---- 合约代码第一部分:从CALLDATALOAD 中获取用户要调用的函数地址并跳转 ----

  3. 031 PUSH1 80

  4. 033 PUSH1 40

  5. 035 MSTORE

  6. 036 PUSH1 04

  7. 038 CALLDATASIZE

  8. 039 LT

组合成的搜索代码如下:

  1. def get_contract_runtime_entry(disassmbly_data) :

  2.    disassmbly_address_list = disassmbly_data.get_disassmbly_address_list()

  3.    disassmbly_address_list_length = disassmbly_data.get_disassmbly_address_list_length()

  4.    for index in range(disassmbly_address_list_length) :

  5.        if  'PUSH1' == disassmbly_data.get_disassmbly_by_address_index(index).get_opcode() and

  6.            'PUSH1' == disassmbly_data.get_disassmbly_by_address_index(index + 1).get_opcode() and

  7.            'MSTORE' == disassmbly_data.get_disassmbly_by_address_index(index + 2).get_opcode() and

  8.            'PUSH1' == disassmbly_data.get_disassmbly_by_address_index(index + 3).get_opcode() and '0x04' == disassmbly_data.get_disassmbly_by_address_index(index + 3).get_opcode_data(0) and

  9.            'CALLDATASIZE' == disassmbly_data.get_disassmbly_by_address_index(index + 4).get_opcode() :

  10.            return disassmbly_address_list[index]

  11.        elif 'PUSH1' == disassmbly_data.get_disassmbly_by_address_index(index).get_opcode() and

  12.            'PUSH1' == disassmbly_data.get_disassmbly_by_address_index(index + 1).get_opcode() and

  13.            'MSTORE' == disassmbly_data.get_disassmbly_by_address_index(index + 2).get_opcode() and

  14.            'CALLDATASIZE' == disassmbly_data.get_disassmbly_by_address_index(index + 3).get_opcode() :

  15.            return disassmbly_address_list[index]

  16.    return -1

分析函数入口点

从上面分析的合约汇编可以知道,PUSH4 + EQ + PUSH1 + JUMPI 是根据函数哈希来进行跳转的代码,我们在合约执行代码获取CALLDATALOAD 之后开始遍历汇编,直至到JUMPDEST + REVERT 指令.

示例合约汇编代码:

  1. 043 PUSH1 00

  2. 045 CALLDATALOAD  ;  CALLDATALOAD 中获取函数hash

  3. 046 PUSH29 0100000000000000000000000000000000000000000000000000000000

  4. 076 SWAP1

  5. 077 DIV

  6. 078 PUSH4 ffffffff

  7. 083 AND

  8. 084 DUP1

  9. ; ---- 把从CALLDATALOAD 中获取的函数哈希来进行比对并跳转 ----

  10. 085 PUSH4 0dbe671f

  11. 090 EQ

  12. 091 PUSH1 4e

  13. 093 JUMPI

  14. 094 DUP1

  15. ; ---- 等价于代码 if (CALLDATALOAD[ : 4] == '0dbe671f') JUMP 0x4E;

  16. 095 PUSH4 cd580ff3

  17. 100 EQ

  18. 101 PUSH1 62

  19. 103 JUMPI

  20. ; ---- 等价于代码 if (CALLDATALOAD[ : 4] == 'cd580ff3') JUMP 0x62;

  21. 104 JUMPDEST

  22. 105 PUSH1 00

  23. 107 DUP1

  24. 108 REVERT  ; 执行合约异常退出并回滚数据

实现代码:

  1. def get_function_entry(disassmbly_data) :

  2.    function_entry_flag = ['PUSH1','CALLDATALOAD']  #  获取CALLDATA 的特征字节码

  3.    disassmbly_address_list = disassmbly_data.get_disassmbly_address_list()

  4.    disassmbly_address_list_length = disassmbly_data.get_disassmbly_address_list_length()

  5.    entry_offset = 0

  6.    for index in range(disassmbly_address_list_length) :

  7.        if index + 2 > disassmbly_address_list_length :

  8.            break

  9.        current_address = disassmbly_address_list[index]

  10.        next_address = disassmbly_address_list[index + 1]

  11.        if  function_entry_flag[0] == disassmbly_data.get_disassmbly_by_address(current_address).get_opcode() and

  12.            function_entry_flag[1] == disassmbly_data.get_disassmbly_by_address(next_address).get_opcode() :

  13.            entry_offset = index + 4

  14.            break

  15.    if not entry_offset :

  16.        return False

  17.    function_entry = function_entry_object({})

  18.    current_function_hash = 0

  19.    for index in range(entry_offset,disassmbly_address_list_length) :

  20.        current_address = disassmbly_address_list[index]

  21.        current_opcode = disassmbly_data.get_disassmbly_by_address(current_address)

  22.        if 'JUMPDEST' == current_opcode.get_opcode() :

  23.             break

  24.         if 'PUSH4' == current_opcode.get_opcode() and not '0xffffffff' == current_opcode.get_opcode_data(0) :

  25.             current_function_hash = current_opcode.get_opcode_data(0)

  26.         elif 'PUSH1' == current_opcode.get_opcode() or 'PUSH2' == current_opcode.get_opcode() :

  27.             if current_function_hash :

  28.                 function_entry.add_function_entry_address(current_function_hash,int(current_opcode.get_opcode_data(0),16))

  29.             current_function_hash = 0

  30.    return function_entry

抽离函数代码

get_function_entry()中获取到各函数对应的入口点地址,那么第一步就需要判断这个地址是否是正确的位置,在EVM 里面规定了JUMP 或者JUMPI 指令跳转的目标指令必须是JUMPDEST 指令,所以第一步就需要去校验入口地址是否为正确.

示例合约汇编代码:

  1. ; ---- 函数"0dbe671f" 入口点代码

  2. 109 JUMPDEST

  3. 110 CALLVALUE

  4. 111 DUP1

  5. 112 ISZERO

  6. 113 PUSH1 59

  7. 115 JUMPI

  8. 116 PUSH1 00

  9. 118 DUP1

  10. 119 REVERT

解析预检测部分的代码:

  1. function_entry_flag = disassmbly_data.get_disassmbly_by_address(function_entry_address).get_opcode()

  2. if not 'JUMPDEST' == function_entry_flag :

  3.    return False

  4. #  1. analayis function pre-check

  5. if 'CALLVALUE' == disassmbly_data.get_disassmbly_by_address(function_entry_address + 1).get_opcode() :

  6.    disassmbly_address_list_offset = disassmbly_address_list.index(function_entry_address + 2)

  7.    function_pre_check_end_offset = 0

  8.    function_argument_check_offset = 0

  9.    for index in range(disassmbly_address_list_offset,disassmbly_address_list_length) :

  10.        current_address = disassmbly_address_list[index]

  11.        if index + 1 == disassmbly_address_list_length :

  12.            break

  13.        next_address = disassmbly_address_list[index + 1]

  14.        #  从PUSH + JUMPI 中得到函数参数获取的跳转地址

  15.        if ('PUSH1' == disassmbly_data.get_disassmbly_by_address(current_address).get_opcode() or 'PUSH2' == disassmbly_data.get_disassmbly_by_address(current_address).get_opcode()) and

  16.            'JUMPI' == disassmbly_data.get_disassmbly_by_address(next_address).get_opcode() :

  17.            function_argument_check_offset = int(disassmbly_data.get_disassmbly_by_address(current_address).get_opcode_data(0),16)

  18.        #  遇到JUMPDEST 指令,也就是指预检测代码到此已经结束了

  19.        if 'JUMPDEST' == disassmbly_data.get_disassmbly_by_address(next_address).get_opcode() :

  20.            function_pre_check_end_offset = current_address

  21.            break

  22.    #  从汇编代码中分割代码块

  23.    function_pre_check_code = disassmbly_data.split_bytecode(function_entry_address,function_pre_check_end_offset)

  24.    #  如果入口点指向的指令不是JUMPDEST ,那就是说跳转地址获取错误

  25.    if not 'JUMPDEST' == disassmbly_data.get_disassmbly_by_address(function_argument_check_offset).get_opcode() :

  26.        return False

  27.    function_code_object.append_bytecode(function_pre_check_code)

  28. else :  #  这不是预检测代码,那把它当作函数参数获取代码来解析

  29.    function_argument_check_offset = function_entry_address

从预检测代码中得到函数参数获取代码之后,接下来从这部分代码中获取函数返回值处理代码和主函数代码.

示例合约汇编代码:

  1. ; 无参数处理的例子

  2. 120 JUMPDEST

  3. 121 POP

  4. 122 PUSH1 60

  5. 124 PUSH1 a0

  6. 126 JUMP

  7. ; ---- 函数参数获取代码,因为这里没有参数,所以可以看到PUSH + (PUSH + JUMP) 的代码.

  8. ; ---- PUSH + JUMP 指的是跳转到0x76 这个位置(等价于JUMP 0x76),这个位置是函数的主入口点

  9. ; ---- PUSH1 60 指向的是返回值处理的代码,在执行完成函数代码之后就会跳转到这里

  10. ; 有一个uint 参数处理的例子

  11. 140 JUMPDEST

  12. 141 POP

  13. 142 PUSH1 8a  ;  指向返回值处理代码

  14. 144 PUSH1 04

  15. 146 DUP1

  16. 147 CALLDATASIZE

  17. 148 SUB

  18. 149 DUP2

  19. 150 ADD

  20. 151 SWAP1

  21. 152 DUP1

  22. 153 DUP1

  23. 154 CALLDATALOAD

  24. 155 SWAP1

  25. 156 PUSH1 20

  26. 158 ADD

  27. 159 SWAP1

  28. 160 SWAP3

  29. 161 SWAP2

  30. 162 SWAP1

  31. 163 POP

  32. 164 POP

  33. 165 POP

  34. 166 PUSH1 a9  ;  指向函数入口点

  35. 168 JUMP

解析函数参数处理代码:

  1. for index in range(disassmbly_address_list_offset,disassmbly_address_list_length) :

  2.    current_address = disassmbly_address_list[index]

  3.    if index + 3 == disassmbly_address_list_length :

  4.        break

  5.    next_address = disassmbly_address_list[index + 1]

  6.    next_next_address = disassmbly_address_list[index + 2]

  7.    next_next_next_address = disassmbly_address_list[index + 3]

  8.    #  获取函数返回值处理的地址

  9.    if (('JUMPDEST' == disassmbly_data.get_disassmbly_by_address(current_address).get_opcode()) and

  10.        ('PUSH1' == disassmbly_data.get_disassmbly_by_address(next_address).get_opcode() or 'PUSH2' == disassmbly_data.get_disassmbly_by_address(next_address).get_opcode()) and

  11.        #('PUSH1' == disassmbly_data.get_disassmbly_by_address(next_next_address).get_opcode() or 'PUSH2' == disassmbly_data.get_disassmbly_by_address(next_next_address).get_opcode()) and

  12.        not function_return_offset) :

  13.        if (('MLOAD' == disassmbly_data.get_disassmbly_by_address(next_next_address).get_opcode()) or

  14.            ('MLOAD' == disassmbly_data.get_disassmbly_by_address(next_next_next_address).get_opcode())) :

  15.            continue

  16.        function_return_offset = int(disassmbly_data.get_disassmbly_by_address(next_address).get_opcode_data(0),16)

  17.    elif (('PUSH1' == disassmbly_data.get_disassmbly_by_address(current_address).get_opcode() or 'PUSH2' == disassmbly_data.get_disassmbly_by_address(current_address).get_opcode()) and

  18.        (disassmbly_data.get_disassmbly_by_address(next_address).get_opcode().startswith('SWAP')) and

  19.        not function_return_offset) :

  20.        function_return_offset = int(disassmbly_data.get_disassmbly_by_address(current_address).get_opcode_data(0),16)

  21.    elif (('POP' == disassmbly_data.get_disassmbly_by_address(current_address).get_opcode()) and

  22.        ('PUSH1' == disassmbly_data.get_disassmbly_by_address(next_address).get_opcode() or 'PUSH2' == disassmbly_data.get_disassmbly_by_address(next_address).get_opcode()) and

  23.        not function_return_offset) :

  24.        if not (disassmbly_data.get_disassmbly_by_address(next_next_address).get_opcode().startswith('DUP')) :

  25.            function_return_offset = int(disassmbly_data.get_disassmbly_by_address(next_address).get_opcode_data(0),16)

  26.    #  获取函数主体代码的地址

  27.    if 'JUMPDEST' == disassmbly_data.get_disassmbly_by_address(next_address).get_opcode() :

  28.        function_argument_check_end_offset = current_address

  29.        #  reverse to search the opcode push ..

  30.        for reverse_index in range(index,disassmbly_address_list_offset,-1) :

  31.            current_address = disassmbly_address_list[reverse_index]

  32.            if ('PUSH1' == disassmbly_data.get_disassmbly_by_address(current_address).get_opcode() or 'PUSH2' == disassmbly_data.get_disassmbly_by_address(current_address).get_opcode()) :

  33.                function_main_c_offset = int(disassmbly_data.get_disassmbly_by_address(current_address).get_opcode_data(0),16)

  34.                break

  35.        break

  36. #  从汇编中分割代码块

  37. function_argument_check_code = disassmbly_data.split_bytecode(function_argument_check_offset,function_argument_check_end_offset)

由于主体函数解析的代码比较繁琐,在此先略过,最后解析返回值处理代码是比较简单易懂的.

示例合约汇编代码:

  1. ; 无参数返回

  2. 127 JUMPDEST

  3. 128 STOP

  4. ; ---- 函数返回值处理代码

  5. ; 有一个uint 参数返回

  6. 169 JUMPDEST

  7. 170 PUSH1 40

  8. 172 MLOAD

  9. 173 DUP1

  10. 174 DUP3

  11. 175 DUP2

  12. 176 MSTORE

  13. 177 PUSH1 20

  14. 179 ADD

  15. 180 SWAP2

  16. 181 POP

  17. 182 POP

  18. 183 PUSH1 40

  19. 185 MLOAD

  20. 186 DUP1

  21. 187 SWAP2

  22. 188 SUB

  23. 189 SWAP1

  24. 190 RETURN

解析返回值处理代码:

  1. for index in range(disassmbly_address_list_offset,disassmbly_address_list_length) :

  2.    current_address = disassmbly_address_list[index]

  3.    if  'STOP' == disassmbly_data.get_disassmbly_by_address(current_address).get_opcode() or

  4.        'RETURN' == disassmbly_data.get_disassmbly_by_address(current_address).get_opcode() :

  5.        function_return_code_end_offset = current_address

  6.        break

至此,从合约汇编中抽离函数代码的思路和实现已经完成,效果如下:

使用Simhash 进行函数相似匹配

对比两个函数的代码是否相似,最主要是对两个函数的汇编操作码进行匹配(忽略操作数的匹配).

  1. def get_features(data) :

  2.    new_data = []

  3.    for opcode_index in data.get_disassmbly_address_list() :

  4.        new_data.append(data.get_disassmbly_by_address(opcode_index).get_opcode())

  5.    return new_data

把汇编操作码提取并序列化出来之后,就使用Simhash() 来对Bytecode 进行计算.

  1. Simhash(get_features(code_data[function_entry_index]))

对于两个合约的所有函数进行比对和匹配,代码如下:

  1. def get_function_simhash(code_data) :

  2.    code_data_simhash_list = {}

  3.    for function_entry_index in code_data :

  4.        function_code = code_data[function_entry_index]

  5.        is_filter = False

  6.        for filter_function_index in filter_function.keys() :  #  filter some function

  7.            if filter_function[filter_function_index](function_code) :

  8.                is_filter = True

  9.                break

  10.        if not is_filter :

  11.            code_data_simhash_list[function_entry_index] = Simhash(get_features(code_data[function_entry_index]))

  12.    return code_data_simhash_list

细心的读者可能会发现,这段函数里面会对合约的函数进行一个过滤,过滤那些没有太大意义的函数,比如mapping() .先来看一段常见的获取余额的代码.

  1. mapping(address => uint256) balances;

  2. function balanceOf(address _owner) public view returns (uint256) {

  3.    return balances[_owner];

  4. }

solidity 编译这段代码之后,结果并不是只有一个函数,而是有两个函数,balances() 和balanceOf() .此时mapping() 其实是被编译成了函数,那么如果两个合约里面都存在mapping() 对象的合约函数,这样势必会影响函数的匹配结果,所以需要去除掉.

把合约里有效的函数计算出Simhash 之后,接下来就需要对两个合约的函数代码进行相似度评分.我们知道,两个文本是一样的话,海明距离的计算结果为0 ,两个文本越来越不相似,那么计算出来的海明距离的值也就越来越大.以此为规则,我们把海明距离的值套用进以小数为底的幂函数里面,当distance 越大,得出来的相似值也就越低.计算公式为:0.935^distance.(此处感谢@k2yk)

  1. def merge_simhash_check(code_data1,code_data2) :

  2.    merge_result = {}

  3.    for code_data1_function_index in code_data1.keys() :

  4.        min_distance = 200  #  no distance bigger than this number ..

  5.        current_function_hash = ''

  6.        for code_data2_function_index in code_data2.keys() :

  7.            current_distance = code_data1[code_data1_function_index].distance(code_data2[code_data2_function_index])

  8.            if current_distance < min_distance :

  9.                min_distance = current_distance

  10.                current_function_hash = code_data2_function_index

  11.        #  I try some simulator function code (modify little code ).If they are simluar ,the distance value is <= 6 .

  12.        merge_result['%s_%s' % (code_data1_function_index,current_function_hash)] = float('%.2f' % pow(DISTANCE_BASE_NUMBER,min_distance))  #  luck nunmber ..

  13.    return merge_result

运行结果示例:

我们收集了一些在以太坊上出现的常见合约脚本,包含众筹代码,token ,去中心化交易所等合约代码.并对整体的以太坊合约代码进行自动分类识别,效果如下:

参考来源

https://www.anquanke.com/post/id/164567

http://www.lanceyan.com/tech/arch/simhash_hamming_distance_similarity.html

https://github.com/googleprojectzero/functionsimsearch

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年1月1日17:01:17
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   函数代码相似性检验之Simhash的应用http://cn-sec.com/archives/1057166.html

发表评论

匿名网友 填写信息