-
-
[原创]零时科技 | 智能合约安全系列文章之反编译篇
-
发表于: 2020-12-4 10:42 12051
-
零时科技 | 智能合约安全系列文章之反编译篇
前言
近年来,各个大型CTF(Capture The Flag,中文一般译作夺旗赛,在网络安全领域中指的是网络安全技术人员之间进行技术竞技的一种比赛形式)比赛中都有了区块链攻防的身影,而且基本都是区块链智能合约攻防。本此系列文章我们也以智能合约攻防为中心,来刨析智能合约攻防的要点,包括合约反编译,CTF常见题型及解题思路,相信会给读者带来不一样的收获。由于CTF比赛中的智能合约源代码没有开源,所以就需要从EVM编译后的opcode进行逆向来得到源代码逻辑,之后根据反编译后的源代码编写攻击合约,最终拿到flag。
基础
本篇我们主要来讲智能合约opcode逆向,推荐的在线工具为Online Solidity Decompiler。该网站逆向的优点比较明显,逆向后会得到合约反编译的伪代码和反汇编的字节码,并且会列出合约的所有函数签名(识别到的函数签名会直接给出,未识别到的会给出UNknown),使用方式为下图:
第一种方式是输入智能合约地址,并选择所在网络
第二钟方式是输入智能合约的opcode
逆向后的合约结果有两个,一种是反编译后的伪代码(偏向于逻辑代码,比较好理解),如下图
另一种是反汇编后的字节码(需要学习字节码相关知识,不容易理解)。
本次演示使用的工具有:
Remix(在线编辑器):https://remix.ethereum.org/
Metamask(谷歌插件):https://metamask.io/
Online Solidity Decompiler(逆向网站):https://ethervm.io/decompile/
案例一
先来看一份简单的合约反编译,合约代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 | pragma solidity ^ 0.4 . 0 ; contract Data { uint De; function set (uint x) public { De = x; } function get() public constant returns (uint) { return De; } } |
编译后得到的opcode如下:
1 | 606060405260a18060106000396000f360606040526000357c01000000000000000000000000000000000000000000000000000000009004806360fe47b11460435780636d4ce63c14605d57603f565b6002565b34600257605b60048080359060200190919050506082565b005b34600257606c60048050506090565b6040518082815260200191505060405180910390f35b806000600050819055505b50565b60006000600050549050609e565b9056 |
利用在线逆向工具反编译后(相关伪代码的含义已在代码段中详细标注):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 | contract Contract { function main() { / / 分配内存空间 memory[ 0x40 : 0x60 ] = 0x60 ; / / 获取data值 var var0 = msg.data[ 0x00 : 0x20 ] / 0x0100000000000000000000000000000000000000000000000000000000 ; / / 判断调用是否和 set 函数签名匹配,如果匹配,就继续执行 if (var0 ! = 0x60fe47b1 ) { goto label_0032; } label_0043: / / 表示不接受msg.value if (msg.value) { label_0002: memory[ 0x40 : 0x60 ] = var0; / / 获取data值 var0 = msg.data[ 0x00 : 0x20 ] / 0x0100000000000000000000000000000000000000000000000000000000 ; / / 判断调用是否和 set 函数签名匹配,如果匹配,就继续执行 / / Dispatch table entry for set (uint256) / / 这里可得知 set 传入的参数类型为uint256 if (var0 = = 0x60fe47b1 ) { goto label_0043; } label_0032: / / 判断调用是否和get函数签名匹配,如果匹配,就继续执行 if (var0 ! = 0x6d4ce63c ) { goto label_0002; } / / 表示不接受msg.value if (msg.value) { goto label_0002; } var var1 = 0x6c ; / / 这里调用get函数 var1 = func_0090(); var temp0 = memory[ 0x40 : 0x60 ]; memory[temp0:temp0 + 0x20 ] = var1; var temp1 = memory[ 0x40 : 0x60 ]; / / if 语句后有 return 表示有返回值,前四行代码都是这里的判断条件,这里返回值最终为var1 return memory[temp1:temp1 + (temp0 + 0x20 ) - temp1]; } else { var1 = 0x5b ; / / 在这里传入的参数 var var2 = msg.data[ 0x04 : 0x24 ]; / / 调用get函数中var2参数 func_0082(var2); stop(); } } / / 下面定义了两个函数,也就是网站列出的两个函数签名 set 和get / / 这里函数传入一个参数 function func_0082(var arg0) { / / slot[ 0 ] = arg0 函数传进来的参数 storage[ 0x00 ] = arg0; } / / 全局变量标记: EVM将合约中的全局变量存放在一个叫Storage的键值对虚拟空间, / / 并且对不同的数据类型有对应的组织方法,存放方式为Storage[keccak256(add, 0x00 )]。 / / storage也可以理解成连续的数组,称为 `slot[]`,每个位置可以存放 32 字节的数据 / / 函数未传入参数,但有返回值 function func_0090() returns (var r0) { / / 这里比较清楚,将上个函数传入的参数slot[ 0 ]的值赋值给var0 var var0 = storage[ 0x00 ]; return var0; / / 最终返回 var0值 } } |
通过上面的伪代码可以得到两个函数set和get。set函数中,有明显的传参arg0,分析主函数main内容后,可得到该函数不接收以太币,并且传入的参数类型为uint256;get函数中,可明显看出未传入参数,但有返回值,也是不接收以太币,通过storage[0x00]的相关调用可以得到返回值为set函数中传入的参数。最终分析伪代码得到的源码如下:
1 2 3 4 5 6 7 8 9 10 11 | contract AAA { uint256 storage; function set (uint256 a) { storage = a; } function get() returns (uint256 storage) { return storage; } } |
相对而言,该合约反编译后的伪代码比较简单,只需要看反编译后的两个函数就可判断出合约逻辑,不过对于逻辑函数较复杂的合约,反编译后的伪代码就需要进一步判断主函数main()中的内容。
案例二
简单入门之后,我们直接来分析一道CTF智能合约的反编译代码
合约地址:https://ropsten.etherscan.io/address/0x93466d15A8706264Aa70edBCb69B7e13394D049f#code
反编译后得到的合约函数签名及方法参数调用如下:
合约伪代码如下(相关伪代码的含义已在代码段中详细标注,标注为重点):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 | contract Contract { function main() { memory[ 0x40 : 0x60 ] = 0x80 ; / / 判断函数签名是否为 4 字节 / / EVM里对函数的调用都是取`bytes4(keccak256(函数名(参数类型 1 ,参数类型 2 ))`传递的,即对函数签名做keccak256哈希后取前 4 字节 if (msg.data.length < 0x04 ) { revert(memory[ 0x00 : 0x00 ]); } / / 取函数签名,前四个字节(函数签名四个字节表示为 0xffffffff 类型) var var0 = msg.data[ 0x00 : 0x20 ] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff ; if (var0 = = 0x2e1a7d4d ) { / / Dispatch table entry for withdraw(uint256) var var1 = msg.value; / / 表示不接受 `msg.value` if (var1) { revert(memory[ 0x00 : 0x00 ]); } var1 = 0x00be ; var var2 = msg.data[ 0x04 : 0x24 ]; withdraw(var2); / / stop表示该函数无返回值 stop(); } else if (var0 = = 0x66d16cc3 ) { / / Dispatch table entry for profit() var1 = msg.value; if (var1) { revert(memory[ 0x00 : 0x00 ]); } var1 = 0x00d5 ; profit(); stop(); } else if (var0 = = 0x8c0320de ) { / / Dispatch table entry for payforflag(string,string) var1 = msg.value; if (var1) { revert(memory[ 0x00 : 0x00 ]); } var1 = 0x0184 ; var temp0 = msg.data[ 0x04 : 0x24 ] + 0x04 ; var temp1 = msg.data[temp0:temp0 + 0x20 ]; var temp2 = memory[ 0x40 : 0x60 ]; memory[ 0x40 : 0x60 ] = temp2 + (temp1 + 0x1f ) / 0x20 * 0x20 + 0x20 ; memory[temp2:temp2 + 0x20 ] = temp1; memory[temp2 + 0x20 :temp2 + 0x20 + temp1] = msg.data[temp0 + 0x20 :temp0 + 0x20 + temp1]; var2 = temp2; var temp3 = msg.data[ 0x24 : 0x44 ] + 0x04 ; var temp4 = msg.data[temp3:temp3 + 0x20 ]; var temp5 = memory[ 0x40 : 0x60 ]; memory[ 0x40 : 0x60 ] = temp5 + (temp4 + 0x1f ) / 0x20 * 0x20 + 0x20 ; memory[temp5:temp5 + 0x20 ] = temp4; memory[temp5 + 0x20 :temp5 + 0x20 + temp4] = msg.data[temp3 + 0x20 :temp3 + 0x20 + temp4]; var var3 = temp5; payforflag(var2, var3); stop(); } else if (var0 = = 0x9189fec1 ) { / / Dispatch table entry for guess(uint256) var1 = msg.value; if (var1) { revert(memory[ 0x00 : 0x00 ]); } var1 = 0x01b1 ; var2 = msg.data[ 0x04 : 0x24 ]; guess(var2); stop(); } else if (var0 = = 0xa5e9585f ) { / / Dispatch table entry for xxx(uint256) var1 = msg.value; if (var1) { revert(memory[ 0x00 : 0x00 ]); } var1 = 0x01de ; var2 = msg.data[ 0x04 : 0x24 ]; xxx(var2); stop(); } else if (var0 = = 0xa9059cbb ) { / / Dispatch table entry for transfer(address,uint256) var1 = msg.value; if (var1) { revert(memory[ 0x00 : 0x00 ]); } var1 = 0x022b ; var2 = msg.data[ 0x04 : 0x24 ] & 0xffffffffffffffffffffffffffffffffffffffff ; var3 = msg.data[ 0x24 : 0x44 ]; transfer(var2, var3); stop(); } else if (var0 = = 0xd41b6db6 ) { / / Dispatch table entry for level(address) var1 = msg.value; if (var1) { revert(memory[ 0x00 : 0x00 ]); } var1 = 0x026e ; var2 = msg.data[ 0x04 : 0x24 ] & 0xffffffffffffffffffffffffffffffffffffffff ; var2 = level(var2); var temp6 = memory[ 0x40 : 0x60 ]; memory[temp6:temp6 + 0x20 ] = var2; var temp7 = memory[ 0x40 : 0x60 ]; / / return 表示该函数有返回值 return memory[temp7:temp7 + (temp6 + 0x20 ) - temp7]; } else if (var0 = = 0xe3d670d7 ) { / / Dispatch table entry for balance(address) var1 = msg.value; if (var1) { revert(memory[ 0x00 : 0x00 ]); } var1 = 0x02c5 ; var2 = msg.data[ 0x04 : 0x24 ] & 0xffffffffffffffffffffffffffffffffffffffff ; var2 = balance(var2); var temp8 = memory[ 0x40 : 0x60 ]; memory[temp8:temp8 + 0x20 ] = var2; var temp9 = memory[ 0x40 : 0x60 ]; return memory[temp9:temp9 + (temp8 + 0x20 ) - temp9]; } else { revert(memory[ 0x00 : 0x00 ]); } } function withdraw(var arg0) { / / 在函数签名处,已给出该函数传参类型为uint256,判断传入的参数arg0是否等于 2 ,如果为 2 ,则继续执行下面代码,否则退出 if (arg0 ! = 0x02 ) { revert(memory[ 0x00 : 0x00 ]); } memory[ 0x00 : 0x20 ] = msg.sender; / / 定义这个msg.sender的第一种类型,可通过balance函数判断出,这里为balance memory[ 0x20 : 0x40 ] = 0x00 ; / / 等同于require(arg0 < = balance[msg.sender]) if (arg0 > storage[keccak256(memory[ 0x00 : 0x40 ])]) { revert(memory[ 0x00 : 0x00 ]); } var temp0 = arg0; var temp1 = memory[ 0x40 : 0x60 ]; / / 将主要内容提取出来,可表示为address(msg.sender).call.gas(msg.gas).value(temp0 * 0x5af3107a4000 ) memory[temp1:temp1 + 0x00 ] = address(msg.sender).call.gas(msg.gas).value(temp0 * 0x5af3107a4000 )(memory[temp1:temp1 + memory[ 0x40 : 0x60 ] - temp1]); memory[ 0x00 : 0x20 ] = msg.sender; memory[ 0x20 : 0x40 ] = 0x00 ; var temp2 = keccak256(memory[ 0x00 : 0x40 ]); / / 可写为storage[temp2] - = temp0, 由之前代码可知temp0 = arg0,由前一句的temp2 = keccak256(memory[ 0x00 : 0x40 ]);向上推理可得知这里为msg.sender storage[temp2] = storage[temp2] - temp0; } function profit() { memory[ 0x00 : 0x20 ] = msg.sender; / / 定义这个msg.sender为第二种类型,可通过level函数判断出,这里为level memory[ 0x20 : 0x40 ] = 0x01 ; / / 这里就等同于require(mapping2[msg.sender] = = 0 ) if (storage[keccak256(memory[ 0x00 : 0x40 ])] ! = 0x00 ) { revert(memory[ 0x00 : 0x00 ]); } memory[ 0x00 : 0x20 ] = msg.sender; / / 启用第一个类型balance进行后续运算 memory[ 0x20 : 0x40 ] = 0x00 ; var temp0 = keccak256(memory[ 0x00 : 0x40 ]); / / 这里进行第一种类型balance的自加一,storage[arg0] + = 1 storage[temp0] = storage[temp0] + 0x01 ; memory[ 0x00 : 0x20 ] = msg.sender; / / 启用第二个类型level进行后续运算 memory[ 0x20 : 0x40 ] = 0x01 ; var temp1 = keccak256(memory[ 0x00 : 0x40 ]); / / 这里进行第二种类型level的自加一,storage[ 0x80 ] + = 1 storage[temp1] = storage[temp1] + 0x01 ; } / / 传入两个string类型的参数 function payforflag(var arg0, var arg1) { memory[ 0x00 : 0x20 ] = msg.sender; / / 启用第一个类型balance进行后续运算 memory[ 0x20 : 0x40 ] = 0x00 ; / / require(balance[msg.sender] > = 0x02540be400 ) if (storage[keccak256(memory[ 0x00 : 0x40 ])] < 0x02540be400 ) { revert(memory[ 0x00 : 0x00 ]); } memory[ 0x00 : 0x20 ] = msg.sender; / / 启用第一个类型balance进行后续运算 memory[ 0x20 : 0x40 ] = 0x00 ; / / 将第一个类型balance赋值为 0 ,等同于balance[msg.sender] = 0 storage[keccak256(memory[ 0x00 : 0x40 ])] = 0x00 ; var temp0 = address(address(this)).balance; var temp1 = memory[ 0x40 : 0x60 ]; var temp2; temp2, memory[temp1:temp1 + 0x00 ] = address(storage[ 0x02 ] & 0xffffffffffffffffffffffffffffffffffffffff ).call.gas(!temp0 * 0x08fc ).value(temp0)(memory[temp1:temp1 + memory[ 0x40 : 0x60 ] - temp1]); var var0 = !temp2; / / 传入一个uint256类型的参数 function guess(var arg0) { if (arg0 ! = storage[ 0x03 ]) { revert(memory[ 0x00 : 0x00 ]); } / / 判断传入的参数是否和storage[ 0x03 ]值匹配, memory[ 0x00 : 0x20 ] = msg.sender; / / 启用第二个类型level进行后续运算 memory[ 0x20 : 0x40 ] = 0x01 ; / / 判断require(mapping1[msg.sender] = = 1 ) if (storage[keccak256(memory[ 0x00 : 0x40 ])] ! = 0x01 ) { revert(memory[ 0x00 : 0x00 ]); } memory[ 0x00 : 0x20 ] = msg.sender; / / 启用第一个类型balance进行后续运算 memory[ 0x20 : 0x40 ] = 0x00 ; var temp0 = keccak256(memory[ 0x00 : 0x40 ]); / / 这里进行第一种类型balance的自加一,storage[ 0x80 ] + = 1 storage[temp0] = storage[temp0] + 0x01 ; memory[ 0x00 : 0x20 ] = msg.sender; / / 启用第二个类型level进行后续运算 memory[ 0x20 : 0x40 ] = 0x01 ; var temp1 = keccak256(memory[ 0x00 : 0x40 ]); / / 这里进行第二种类型level的自加一,storage[ 0x80 ] + = 1 storage[temp1] = storage[temp1] + 0x01 ; } function xxx(var arg0) { / / storage[ 0x02 ] & 0xffffffffffffffffffffffffffffffffffffffff 表示storage[ 0x02 ]为一个地址类型 / / 判断调用者发起人的地址是否为匹配 if (msg.sender ! = storage[ 0x02 ] & 0xffffffffffffffffffffffffffffffffffffffff ) { revert(memory[ 0x00 : 0x00 ]); } / / 将传入的uint256数值赋值给storage[ 0x03 ] storage[ 0x03 ] = arg0; } / / 传入两个参数分别为address和uint256 function transfer(var arg0, var arg1) { memory[ 0x00 : 0x20 ] = msg.sender; / / 启用第一个类型balance进行后续运算 memory[ 0x20 : 0x40 ] = 0x00 ; / / 这里为require(balance[msg.sender] > = arg1) if (storage[keccak256(memory[ 0x00 : 0x40 ])] < arg1) { revert(memory[ 0x00 : 0x00 ]); } / / 判断arg1是否等于 2 ,require(arg1 = = 2 ) if (arg1 ! = 0x02 ) { revert(memory[ 0x00 : 0x00 ]); } memory[ 0x00 : 0x20 ] = msg.sender; / / 启用第二个类型level进行后续运算 memory[ 0x20 : 0x40 ] = 0x01 ; if (storage[keccak256(memory[ 0x00 : 0x40 ])] ! = 0x02 ) { revert(memory[ 0x00 : 0x00 ]); } / / 判断条件,为require(level[msg.sender] = = 2 ) memory[ 0x00 : 0x20 ] = msg.sender; / / 启用第一个类型balance进行后续运算 memory[ 0x20 : 0x40 ] = 0x00 ; / / 赋值操作:balance[msg.sender] = 0 storage[keccak256(memory[ 0x00 : 0x40 ])] = 0x00 ; memory[ 0x00 : 0x20 ] = arg0 & 0xffffffffffffffffffffffffffffffffffffffff ; / / 启用第一个类型balance进行后续运算 memory[ 0x20 : 0x40 ] = 0x00 ; / / balance[address] = arg1 storage[keccak256(memory[ 0x00 : 0x40 ])] = arg1; } function level(var arg0) returns (var arg0) { memory[ 0x20 : 0x40 ] = 0x01 ; memory[ 0x00 : 0x20 ] = arg0; return storage[keccak256(memory[ 0x00 : 0x40 ])]; } function balance(var arg0) returns (var arg0) { memory[ 0x20 : 0x40 ] = 0x00 ; memory[ 0x00 : 0x20 ] = arg0; return storage[keccak256(memory[ 0x00 : 0x40 ])]; } } |
通过分析上面经过详细标注的反编译伪代码,我们写出合约源码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | contract babybank { address owner; uint secret; event sendflag(string base1,string base2); constructor()public{ owner = msg.sender; } function payforflag(string base1,string base2) public{ require(balance[msg.sender] > = 10000000000 ); balance[msg.sender] = 0 ; owner.transfer(address(this).balance); emit sendflag(base1,base2); } modifier onlyOwner(){ require(msg.sender = = owner); _; } function withdraw(uint256 amount) public { require(amount = = 2 ); require(amount < = balance[msg.sender]); address(msg.sender).call.gas(msg.gas).value(amount * 0x5af3107a4000 )(); balance[msg.sender] - = amount; } function profit() public { require(level[msg.sender] = = 0 ); balance[msg.sender] + = 1 ; level[msg.sender] + = 1 ; } function xxx(uint256 number) public onlyOwner { secret = number; } function guess(uint256 number) public { require(number = = secret); require(level[msg.sender] = = 1 ); balance[msg.sender] + = 1 ; level[msg.sender] + = 1 ; } function transfer(address to, uint256 amount) public { require(balance[msg.sender] > = amount); require(amount = = 2 ); require(level[msg.sender] = = 2 ); balance[msg.sender] = 0 ; balance[to] = amount; } } |
该反编译合约中,需要判断分析的点为合约中的逻辑函数和主函数main()的相关判断。逻辑函数(withdraw,profit,payforflag,guess,xxx,transfer)中和主函数main()需要关注的点为:
memory[0x20:0x40] = 0x00和memory[0x20:0x40] = 0x01分别代表balance和level
if (arg1 != 0x02) { revert(memory[0x00:0x00]); }代表require(arg1 == 2),其他条件判断与此相似
if (msg.sender != storage[0x02] & 0xffffffffffffffffffffffffffffffffffffffff) { revert(memory[0x00:0x00]); } 表示为require(msg.sender == owner)
storage[temp1] = storage[temp1] + 0x01;表示为level[msg.sender] += 1;
if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); } //判断函数签名是否为4字节
var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff; //取函数签名,前四个字节(函数签名四个字节表示为0xffffffff类型) ,EVM里对函数的调用都是取
bytes4(keccak256(函数名(参数类型1,参数类型2))
传递的,即对函数签名做keccak256哈希后取前4字节if (var1) { revert(memory[0x00:0x00]); } //表示不接受
msg.value
stop(); //stop表示该函数无返回值
return memory[temp7:temp7 + (temp6 + 0x20) - temp7]; //return表示该函数有返回值
总结
本篇主要分享的内容为,通过在线网站反编译智能合约opcode的一种方法,比较适合新手学习,下一篇我们会继续分享逆向智能合约的反汇编手法,希望对读者有所帮助。
[培训]内核驱动高级班,冲击BAT一流互联网大厂工作,每周日13:00-18:00直播授课