说起以太坊的智能合约,因为区块链上所有的数据都是公开透明的,所以合约的代码也都是公开的。但是其实它公开的都是经过编译的OPCODE,真正的源代码公开与否就得看发布合约的人了。如果要真正的掌握一个合约会干什么,就得从OPCODE逆向成solidity代码。下面进行练手和实战,实战的是今年PHDays安全会议的比赛里的一道题。
在etherscan上看到的合约的代码示例:
现在网上免费的工具不太多,我会在结尾贴出我知道的其他工具。 在这里我用的是IDA-EVM(半年没更新啦)和ethervm.io的反编译工具:IDA-EVMethervm 如果要查手册:solidity手册 用来查一些EVM的特性OPCODE 用来查OPCODE的特性
编译后:
6080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630dbe671f14604e5780634df7e3d0146076575b600080fd5b348015605957600080fd5b506060608a565b6040518082815260200191505060405180910390f35b348015608157600080fd5b5060886098565b005b60006094600260ab565b5090565b6000808154809291906001019190505550565b600060018211151560be576001905060cd565b60c86001830360ab565b820290505b9190505600a165627a7a7230582016d61ab556bcec17631dad0b32eae60becb475ff83dae1ed39cbecde69e0cec00029
反编译后:
contract Contract { function main() { memory[0x40:0x60] = 0x80; if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); } var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff; if (var0 == 0x0dbe671f) { // Dispatch table entry for a() var var1 = msg.value; if (var1) { revert(memory[0x00:0x00]); }//因为没有payable修饰,不能接收eth var1 = 0x5f; var1 = a(); var temp0 = memory[0x40:0x60];//0x80 memory[temp0:temp0 + 0x20] = var1;//[0x80:0xa0]=a() var temp1 = memory[0x40:0x60];//0x80 return memory[temp1:temp1 + temp0 - temp1 + 0x20];//return [0x80:0xa0] 也就是a() } else if (var0 == 0x4df7e3d0) { // Dispatch table entry for b() var1 = msg.value; if (var1) { revert(memory[0x00:0x00]); } var1 = 0x83; b(); stop(); } else { revert(memory[0x00:0x00]); } } function a() returns (var r0) { var var0 = 0x00; var var1 = 0x8f; var var2 = 0x02; var1 = func_009E(var2); return var0; } function b() { storage[0x00] = storage[0x00] + 0x01; } function func_009E(var arg0) returns (var r0) { var var0 = 0x00; if (arg0 > 0x01) { var var1 = 0xb8; var var2 = arg0 - 0x01; var1 = func_009E(var2); var0 = arg0 * var1; label_00BD: return var0; } else { var0 = 0x01; goto label_00BD; } } }第一句if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); }是因为EVM里对函数的调用都是取bytes4(keccak256(函数名(参数类型1,参数类型2))传递的,即对函数签名做keccak256哈希后取前4字节。
下一行对msg.data进行 & 0xffffffff就是这个作用,到下一个if语句下面,我们会发现有个// Dispatch table entry for a(),这其实是反编译器自动注释的。在 Ethereum Function Signature Database 这个网站里有对各种函数签名进行keccak256()的数据库,如果在反编译的时候就会自动查询并注释,知道函数签名会大大方便我们的工作。
从 if (var1) { revert(memory[0x00:0x00]); 这个语句里,我们就可以知道函数 a()是没有paypable修饰的。然后转进a(),又转进func_009E(2),这里其实就是一个if语句,递归自己,当参数小于等于1的时候返回1。仔细一想,这有点像阶乘嘛。至于b(),就只有一个storage[0x00]位自加一了。 然后试着写一下,清楚多了:
contract gogogo{ function a() public returns(uint) { func_009E(2) return 0 } function b()public { storage[0x00] += 0x01; return 0;//和stop()等价 } function func_009E( arg0) returns ( r0){ if(arg0<=1) { return 1; } return arg0*func_009E(arg0-1) } }这个是一个有漏洞的合约。
pragma solidity ^0.4.21; contract TokenSaleChallenge { mapping(address => uint256) public balanceOf; uint256 constant PRICE_PER_TOKEN = 1 ether; function TokenSaleChallenge(address _player) public payable { require(msg.value == 1 ether); } function isComplete() public view returns (bool) { return address(this).balance < 1 ether; } function buy(uint256 numTokens) public payable { require(msg.value == numTokens * PRICE_PER_TOKEN); balanceOf[msg.sender] += numTokens; } function sell(uint256 numTokens) public { require(balanceOf[msg.sender] >= numTokens); balanceOf[msg.sender] -= numTokens; msg.sender.transfer(numTokens * PRICE_PER_TOKEN); } }反编译后:
contract Contract { function main() { memory[0x40:0x60] = 0x80; if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); } var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff; if (var0 == 0x70a08231) { // Dispatch table entry for balanceOf(address) var var1 = msg.value; if (var1) { revert(memory[0x00:0x00]); } var1 = 0x0094; var var2 = msg.data[0x04:0x24] & 0xffffffffffffffffffffffffffffffffffffffff; var2 = balanceOf(var2); var temp0 = memory[0x40:0x60]; memory[temp0:temp0 + 0x20] = var2; var temp1 = memory[0x40:0x60]; return memory[temp1:temp1 + temp0 - temp1 + 0x20]; } else if (var0 == 0xb2fa1c9e) { // Dispatch table entry for isComplete() var1 = msg.value; if (var1) { revert(memory[0x00:0x00]); } var1 = 0x00bb; var1 = isComplete(); var temp2 = memory[0x40:0x60]; memory[temp2:temp2 + 0x20] = !!var1; var temp3 = memory[0x40:0x60]; return memory[temp3:temp3 + temp2 - temp3 + 0x20]; } else if (var0 == 0xd96a094a) { // Dispatch table entry for buy(uint256) var1 = 0x00da; var2 = msg.data[0x04:0x24]; buy(var2); stop(); } else if (var0 == 0xe4849b32) { // Dispatch table entry for sell(uint256) var1 = msg.value; if (var1) { revert(memory[0x00:0x00]); } var1 = 0x00da; var2 = msg.data[0x04:0x24]; sell(var2); stop(); } else { revert(memory[0x00:0x00]); } } function balanceOf(var arg0) returns (var arg0) { memory[0x20:0x40] = 0x00; memory[0x00:0x20] = arg0; return storage[keccak256(memory[0x00:0x40])]; } function isComplete() returns (var r0) { return address(address(this)).balance < 0x0de0b6b3a7640000; } function buy(var arg0) { if (arg0 * 0x0de0b6b3a7640000 != msg.value) { revert(memory[0x00:0x00]); } memory[0x00:0x20] = msg.sender; memory[0x20:0x40] = 0x00; var temp0 = keccak256(memory[0x00:0x40]); storage[temp0] = arg0 + storage[temp0]; } function sell(var arg0) { memory[0x00:0x20] = msg.sender; memory[0x20:0x40] = 0x00; if (arg0 > storage[keccak256(memory[0x00:0x40])]) { revert(memory[0x00:0x00]); } var temp0 = msg.sender; memory[0x00:0x20] = temp0; memory[0x20:0x40] = 0x00; var temp1 = keccak256(memory[0x00:0x40]); var temp2 = arg0; storage[temp1] = storage[temp1] - temp2; var temp3 = memory[0x40:0x60]; var temp4 = temp2 * 0x0de0b6b3a7640000; memory[temp3:temp3 + 0x00] = address(temp0).call.gas(!temp4 * 0x08fc).value(temp4)(memory[temp3:temp3 + 0x00]); var var0 = !address(temp0).call.gas(!temp4 * 0x08fc).value(temp4)(memory[temp3:temp3 + 0x00]); if (!var0) { return; } var temp5 = returndata.length; memory[0x00:0x00 + temp5] = returndata[0x00:0x00 + temp5]; revert(memory[0x00:0x00 + returndata.length]); } }首先看balanceOf(address),这里需要知道映射的储存方式,映射mapping 中的键 k 所对应的值会位于 keccak256(k.p), 其中 . 是连接符。
通过var2在定义的时候& 0xffffffffffffffffffffffffffffffffffffffff可以确定var2是address类型(160位),传入balanceOf()后可以发现返回storage[keccak256(address+0x00)],从+0x00可以看出来这就是最开始就定义的从地址到某个类型(分析其他地方可得出)的映射。 isComplete()非常明了了,直接跳过。 buy(arg0)第一行明显要我们传入 arg0 数量的 ether 进去,这里可以还原成require() 下面的代码把arg0加到上面的映射中。再结合函数名(这次运气好函数名都有)可以猜出这个合约到底要干啥了。
sell()里要注意的是 memory[temp3:temp3 + 0x00] = address(temp0).call.gas(!temp4 * 0x08fc).value(temp4)(memory[temp3:temp3 + 0x00]);这一行包括下面的代码都是代表一个transfer(),因为transfer要处理返回值,所以分开写看起来比较多。这里我们可以看到安全的transfer也是通过.call.value来实现的,只不过对gas做了严格控制,杜绝重入漏洞。 试着写一下:
contract gogogo{ function balanceOf(address add)public returns (){ return storage[keccak256(add+0x00)]; //映射的储存方法 //也就是 mapping(address => uint256) public balanceOf; } function isComplete(){ return address(this).balance < 1 ether; } function buy( arg0)public { require(msg.value==arg0*1 ether); mapping[msg.sender] += arg0; } function sell( arg0)public{ require(mapping[msg.sender]>=arg0) mapping[msg.sender] -=arg0; msg.sender.transfer(arg0 * 1 ehter); } }最后说一下,这段代码是require(msg.value==arg0*1 ether);有溢出点,可以绕过。
实战的是今年PHDays安全会议的比赛里的一道逆向题The Lock。这道题没有源码(废话)。 做这个题的时候我们要再加上IDA-EVM,更方便分析。 已知信息:解锁这个合约就胜利,函数签名unlock(bytes4 pincode),每次尝试支付0.5 ehter 直接上反编译后的:
contract Contract { function main() { memory[0x40:0x60] = 0x60; if (msg.data.length < 0x04) { revert(memory[0x00:0x00]); } var var0 = msg.data[0x00:0x20] / 0x0100000000000000000000000000000000000000000000000000000000 & 0xffffffff; if (var0 == 0x6a5e2650) { // Dispatch table entry for unlocked() if (msg.value) { revert(memory[0x00:0x00]); } var var1 = 0x0064; var var2 = unlocked(); var temp0 = memory[0x40:0x60]; memory[temp0:temp0 + 0x20] = !!var2; var temp1 = memory[0x40:0x60]; return memory[temp1:temp1 + (temp0 + 0x20) - temp1]; } else if (var0 == 0x75a4e3a0) { // Dispatch table entry for 0x75a4e3a0 (unknown) //unlock(bytes4) var1 = 0x00b3; var2 = msg.data[0x04:0x24] & ~0xffffffffffffffffffffffffffffffffffffffffffffffffffffffff; var1 = func_00DF(var2); var temp2 = memory[0x40:0x60]; memory[temp2:temp2 + 0x20] = !!var1; var temp3 = memory[0x40:0x60]; return memory[temp3:temp3 + (temp2 + 0x20) - temp3]; } else { revert(memory[0x00:0x00]); } } function unlocked() returns (var r0) { return storage[0x00] & 0xff; } //unlock(bytes4 ) function func_00DF(var arg0) returns (var r0) { var var0 = 0x00; var var1 = var0; var var2 = 0x00; var var3 = var2; var var4 = 0x00; var var5 = var4; if (msg.value < 0x06f05b59d3b20000) { revert(memory[0x00:0x00]); } var3 = 0x00; if (var3 & 0xff >= 0x04) { label_01A4: if (var2 != var1) { return 0x00; } storage[0x00] = (storage[0x00] & ~0xff) | 0x01; return 0x01; } else { label_0111: var var6 = arg0; var var7 = var3 & 0xff;//0x00 if (var7 >= 0x04) { assert(); } var4 = (byte(var6, var7) * 0x0100000000000000000000000000000000000000000000000000000000000000) / 0x0100000000000000000000000000000000000000000000000000000000000000; var6 = var4 >= 0x30; if (!var6) { if (!var6) { label_0197: var3 = var3 + 0x01; label_0104: if (var3 & 0xff >= 0x04) { goto label_01A4; } else { goto label_0111; } } else { label_0181: var temp0 = var4 - 0x30; var5 = temp0; var2 = var2 + var5 ** 0x04; var1 = var1 * 0x0a + var5; var3 = var3 + 0x01; goto label_0104; } } else if (var4 > 0x39) { goto label_0197; } else { goto label_0181; } } } }先修正一个错误!反编译后的byte(var6, var7)里两个参数的位置是错误的,byte(var7, var6)应该是这样.这个地方搞了我好久,但是人家也标注了工具是"experimental"性质的嘛。byte()的作用是把栈顶替换成栈顶下面一个元素的第栈顶值个字节的值。byte()的图解
整体看下来目的比较明确,就是要通过func_00DF(var arg0)函数把storage[0x00]改为1,使unlocked()返回1即可。我们先在IDA里看看,发现进入label_104代码段后会进入一个大循环。
开门先判断 var7>=4 ,下面又使var4为输入的第var7个字节。var6判断这个字节是不是大于0x30,可以猜出来出来0x30是0的ASCII码,应该有点关系。发现当var6为1时,会判断var4是否大于0x39,这不就是9的ascii码么.然后我们从label_0197开始看,发现如果不符合要求var3自加一后会继续循环,直到它为4时进入可以改变storage[0x00]的代码段。再看一下label_0181代码段,这里就是把提取出的单字节字符转换成数字后,var2加上它的4次方,var1加上它的10倍。 分析到这里,就可以试着写一下大致逻辑了:
contract gogogo{ uint8 isunLocke; function unlocked() public { return isunLocke; } function unlock(arg0) payable public{ require(msg.sender.value>=0.5 ether); for(i = 0; i < 4; i++ ){ chr = byte(i,arg0) if(chr < 0x30 || chr > 0x39 ){ continue; } number = chr - 0x30 //字符转数字 var2 = var2 + number**4 var1 = var1 + number*10 } if(var2 != var1){ return } isunLocke = 1; //(0 & ~0xff) | 0x01 //1 } }可以看出,我们要提供各位4次方之和为它本身的数字。稍稍爆破一下就有1634可以满足
从逆向的过程来看,要熟知EVM的各种原理,比如各种不同的变量的储存方式等等。看逆向的代码也能帮我们更深入的了解solidity的一些漏洞,比如变量覆盖,很明显我们写的storage变量在编译后全都变成对storage位的操作了,运用不当肯定会造成变量覆盖漏洞嘛。
其他工具:https://github.com/radare/radare2https://github.com/radare/radare2-extrashttps://github.com/trailofbits/ethersplayhttps://github.com/meyer9/ethdasmhttps://github.com/comaeio/porosity