前言

说起以太坊的智能合约,因为区块链上所有的数据都是公开透明的,所以合约的代码也都是公开的。但是其实它公开的都是经过编译的OPCODE,真正的源代码公开与否就得看发布合约的人了。如果要真正的掌握一个合约会干什么,就得从OPCODE逆向成solidity代码。下面进行练手和实战,实战的是今年PHDays安全会议的比赛里的一道题。

在etherscan上看到的合约的代码示例:

工欲善其事,必先利其器

现在网上免费的工具不太多,我会在结尾贴出我知道的其他工具。
在这里我用的是IDA-EVM(半年没更新啦)和ethervm.io的反编译工具:
IDA-EVM
ethervm
如果要查手册:
solidity手册
用来查一些EVM的特性
OPCODE
用来查OPCODE的特性

练手1

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)
    }
}

练手2

这个是一个有漏洞的合约。

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

作者水平有限,如有错误和欠妥之处,恳请各位师傅指正!

源链接

Hacking more

...