说起以太坊的智能合约,因为区块链上所有的数据都是公开透明的,所以合约的代码也都是公开的。但是其实它公开的都是经过编译的OPCODE,真正的源代码公开与否就得看发布合约的人了。如果要真正的掌握一个合约会干什么,就得从OPCODE逆向成solidity代码。下面进行练手和实战,实战的是今年PHDays安全会议的比赛里的一道题。
在etherscan上看到的合约的代码示例:
现在网上免费的工具不太多,我会在结尾贴出我知道的其他工具。
在这里我用的是IDA-EVM(半年没更新啦)和ethervm.io的反编译工具:
IDA-EVM
ethervm
如果要查手册:
solidity手册
用来查一些EVM的特性
OPCODE
用来查OPCODE的特性
contract Demo {
uint256 private c;
function a() public returns (uint256) { factorial(2); }
function b() public { c++; }
function factorial(uint n) internal returns (uint256) {
if (n <= 1) { return 1; }
return n * factorial(n - 1);
}
}
编译后:
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/radare2
https://github.com/radare/radare2-extras
https://github.com/trailofbits/ethersplay
https://github.com/meyer9/ethdasm
https://github.com/comaeio/porosity
作者水平有限,如有错误和欠妥之处,恳请各位师傅指正!