以太坊上的智能合约几乎都是开源的,没有开源的智能合约就无从信任。但有些智能合约没有开源,反编译是研究的重要方式,可通过直接研究EVM的ByteCode。
如何对合约进行逆向分析,下面结合ctf实例介绍区块链合约逆向如何开展,希望区块链入门者能从中学到知识。


ctf实例1

给了bytecode字节码及交互记录

ByteCode:

0x60806040526004361061006d576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806304618359146100725780631cbeae5e1461009f578063890eba68146100cc578063a2da82ab146100f7578063f0fdf83414610127575b600080fd5b34801561007e57600080fd5b5061009d60048036038101908080359060200190929190505050610154565b005b3480156100ab57600080fd5b506100ca6004803603810190808035906020019092919050505061015e565b005b3480156100d857600080fd5b506100e1610171565b6040518082815260200191505060405180910390f35b34801561010357600080fd5b50610125600480360381019080803560ff169060200190929190505050610177565b005b34801561013357600080fd5b50610152600480360381019080803590602001909291905050506101bb565b005b8060008190555050565b6000548114151561016e57600080fd5b50565b60005481565b60008060009150600090505b60108110156101ab576008829060020a0291508260ff16821891508080600101915050610183565b8160005418600081905550505050565b8060036000540201600081905550505600a165627a7a7230582012c9c1368a7902a818e339b8db79b7130db8795bd2a793898b509dc020d960d20029

交互日志:

log1:func_0177
0xa2da82ab0000000000000000000000000000000000000000000000000000000000000009

log2: #a()
0xf0fdf83400000000000000000000000000000000000000000000000000000000deadbeaf

log3: #func_0177
0xa2da82ab0000000000000000000000000000000000000000000000000000000000000007

log4: #flag()
secret.flag
{
    "0": "uint256: 36269314025157789027829875601337027084"
}

在线反编译

https://ethervm.io/decompile 反编译bytecode
直接输入bytecode(不要加0x,输入十六进制值即可)

反编译得到

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 == 0x04618359) {
            // Dispatch table entry for 0x04618359 (unknown)
            var var1 = msg.value;

            if (var1) { revert(memory[0x00:0x00]); }

            var1 = 0x009d;
            var var2 = msg.data[0x04:0x24];
            func_0154(var2);
            stop();
        } else if (var0 == 0x1cbeae5e) {
            // Dispatch table entry for winner(uint256)
            var1 = msg.value;

            if (var1) { revert(memory[0x00:0x00]); }

            var1 = 0x00ca;
            var2 = msg.data[0x04:0x24];
            winner(var2);
            stop();
        } else if (var0 == 0x890eba68) {
            // Dispatch table entry for flag()
            var1 = msg.value;

            if (var1) { revert(memory[0x00:0x00]); }

            var1 = 0x00e1;
            var2 = flag();
            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 == 0xa2da82ab) {
            // Dispatch table entry for 0xa2da82ab (unknown)
            var1 = msg.value;

            if (var1) { revert(memory[0x00:0x00]); }

            var1 = 0x0125;
            var2 = msg.data[0x04:0x24] & 0xff;
            func_0177(var2);
            stop();
        } else if (var0 == 0xf0fdf834) {
            // Dispatch table entry for a(uint256)
            var1 = msg.value;

            if (var1) { revert(memory[0x00:0x00]); }

            var1 = 0x0152;
            var2 = msg.data[0x04:0x24];
            a(var2);
            stop();
        } else { revert(memory[0x00:0x00]); }
    }

    function func_0154(var arg0) {
        storage[0x00] = arg0;
    }

    function winner(var arg0) {
        if (arg0 == storage[0x00]) { return; }
        else { revert(memory[0x00:0x00]); }
    }

    function flag() returns (var r0) { return storage[0x00]; }

    function func_0177(var arg0) {
        var var0 = 0x00;
        var var1 = 0x00;

        if (var1 >= 0x10) {
        label_01AB:
            storage[0x00] = storage[0x00] ~ var0; //这里~符号应为异或 xor
            return;
        } else {
        label_018D:
            var0 = var0 * 0x02 ** 0x08 ~ (arg0 & 0xff); 
            var1 = var1 + 0x01;

            if (var1 >= 0x10) { goto label_01AB; }
            else { goto label_018D; }
        }
    }

    function a(var arg0) {
        storage[0x00] = storage[0x00] * 0x03 + arg0;
    }
}

ethervm.io也给出了函数的调用情况

--Public Methods
Method names cached from 4byte.directory.
0x04618359 Unknown #func_0154
0x1cbeae5e winner(uint256)
0x890eba68 flag()
0xa2da82ab Unknown #func_0177
0xf0fdf834 a(uint256)

--Internal Methods
func_0154(arg0) 
winner(arg0) 
flag(arg0) returns (r0)
func_0177(arg0) 
a(arg0)

可以看到,总共有5个公用(public)函数调用接口。第一个 0x04618359 和第四个0xa2da82ab没有查到历史函数名称,说明是合约开发者自己定义的,这里反编译器把它命名为 func_0154func_0177 。其他函数还有winner,flag,a

观察日志交互记录
0xa2da82ab0000000000000000000000000000000000000000000000000000000000000009前面的8位为函数的地址0xa2da82ab,对应func_0177函数,传参为0x09。
0xf0fdf83400000000000000000000000000000000000000000000000000000000deadbeaf对应调用函数a(),传参为0xdeadbeaf
日志最后返回的secret.flag应为执行flag()返回的值36269314025157789027829875601337027084

程序调用逻辑即为分别执行func_0177(0x9),a(0xdeadbeaf),func_0177(0x7),flag()

需要求解的为输入的值,那么进行逆向即可

观察三个函数,都是比较简单的运算,等价于下面大马

#输入参数x
def func_0177(var=0x9):
  var=9
  a=0
  b=0
  for i in range(0x10):
     a=a*(2**8)^(var&0xff)

x=x^a

def a(y=0xdeadbeaf)
   x=x*3+0xdeadbeaf

def func_0177(var=0x7)
  var=7
  a=0
  b=0
  for i in range(0x10):
     a=a*(2**8)^(var&0xff)

x=x^a

def flag():
  return x

#返回结果为:
secret.flag
{
    "0": "uint256: 36269314025157789027829875601337027084"
}

那简单逆向即可,func_0177计算的异或参数确定,直接异或即得到原值,逆向代码如下

x=36269314025157789027829875601337027084
var=7
a=0
b=0
for i in range(0x10):
    a=a*(2**8)^(var&0xff)

x=x^a
x=(x-0xdeadbeaf)/3

var=9
a=0
b=0
for i in range(0x10):
    a=a*(2**8)^(var&0xff)

x=x^a

print hex(x)[2].strip('L').decode('hex')
#flag{hello_ctf}

jeb反编译

如果是线下ctf比赛,无法在线反编译,可以准备jeb,尽管是demo版,也基本够用

jeb3.7

直接将bytecode保存到文件,jeb选择菜单文件中的Open smart contract, 选择本地文件即可, 反编译代码如下

function start() {
    *0x40 = 0x80;
    var1 = msg.data.length;

    if(var1 >= 0x4) {
        uint256 var0 = (uint256)$msg.sig;

        if(var0 == 0x4618359) {
            sub_72();
        }

        if(var0 == 0x1cbeae5e) {
            winner();
        }

        if(var0 == 0x890eba68) {
            flag();
        }

        if(var0 == 0xa2da82ab) {
            sub_F7();
        }

        if(var0 == 0xf0fdf834) {
            a();
        }
    }

    revert(0x0, 0x0);
}

sub_F7()

function sub_F7() public /*NON-PAYABLE*/ {
    var3 = msg.data.length;
    var4 = calldataload(0x4);
    sub_177(var4 & 0xff);
    stop();
}

function sub_177(uint256 par1) private {
    int256 var0 = 0x0;

    for(uint256 var1 = 0x0; var1 < 0x10; ++var1) {
        var0 = (var0 * 0x100) ^ (par1 & 0xff);
    }

    var3 = storage[0x0];
    g0_0 = var0 ^ var3;
}

a()

function a() public /*NON-PAYABLE*/ {
    var3 = msg.data.length;
    var4 = calldataload(0x4);
    __impl_a(var4);
    stop();
}

function __impl_a(uint256 par1) private {
    var2 = storage[0x0];
    g0_0 = var2 * 0x3 + par1;
}

flag()

function flag() public view /*NON-PAYABLE*/ {
    (uint256 var0, uint256 var1) = __impl_flag();
    uint256* var3 = *0x40;
    *var3 = var1;
    return(*0x40, var3 + 1 - *0x40);
}

function __impl_flag() private view returns (uint256) {
    var0 = storage[0x0];
    return var0;
}

可看出反编译效果不错,很容易理解算法。

ctf实例2

题目内容

send 1505 szabo 457282 babbage 649604 wei 0x949a6ac29b9347b3eb9a420272a9dd7890b787a3

再ethereum mainnet查看合约地址0x949a6ac29b9347b3eb9a420272a9dd7890b787a3

访问https://etherscan.io/address/0x949a6ac29b9347b3eb9a420272a9dd7890b787a3

查看contract对应bytecode为

0x606060405260043610610057576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680632a0f76961461005c5780635b6b431d1461009f5780639f1b3bad146100c2575b600080fd5b341561006757600080fd5b610081600480803561ffff169060200190919050506100cc565b60405180826000191660001916815260200191505060405180910390f35b34156100aa57600080fd5b6100c06004808035906020019091905050610138565b005b6100ca6101d6565b005b60006001546001900461ffff168261ffff16141561012b57600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020549050610133565b600060010290505b919050565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614151561019357600080fd5b3373ffffffffffffffffffffffffffffffffffffffff166108fc829081150290604051600060405180830381858888f1935050505015156101d357600080fd5b50565b6000806002346000604051602001526040518082815260200191505060206040518083038160008661646e5a03f1151561020f57600080fd5b50506040518051905091506001548218905080600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020816000191690555050505600a165627a7a723058204760a4fe708c70459c1c33c4668609c3f1a8cf0a82d2fc7786c343457dbb55c30029

用jeb3.7 demo反编译一下bytecode

function Withdraw() public /*NON-PAYABLE*/ {
    var3 = calldataload(0x4);
    __impl_Withdraw(var3);
    stop();
}

function __impl_Withdraw(uint256 par1) private {
    var1 = storage[0x0];
    int256 var0 = var1;
    var1 = msg.sender;

    if((address(var0)) != (address(msg.sender))) {
        revert(0x0, 0x0);
    }

    var0 = msg.sender;
    var4 = send(address(msg.sender), par1);

    if(var4 == 0x0) {
        revert(0x0, 0x0);
    }
}

function Receive() public payable {
    __impl_Receive();
    stop();
}

function __impl_Receive() private {
    *(*0x40 + 0x20) = 0x0;
    int256 var5 = *0x40;
    *var5 = $msg.value;
    var11 = gasleft();
    var4 = call_sha256(var11 - 0x646e, 0x2, 0x0, var5, var5 + 0x20 - var5, var5, 0x20);

    if(var4 == 0x0) {
        revert(0x0, 0x0);
    }

    var2 = storage[0x1];
    var2 ^= **0x40;
    var5 = msg.sender;
    *0x0 = address(msg.sender);
    *0x20 = 0x2;
    var3 = keccak256(0x0, 0x40);
    storage[var3] = var2;
}

function sub_5C() public view /*NON-PAYABLE*/ {
    var3 = calldataload(0x4);
    uint256 var0 = sub_CC(var3 & 0xffff);
    uint256* var2 = *0x40;
    *var2 = var0;
    return(*0x40, var2 + 1 - *0x40);
}


function sub_CC(uint256 par1) private view returns (uint256) {
    uint256 var0;
    var1 = storage[0x1];

    if((par1 & 0xffff) == (var1 & 0xffff)) {
        var3 = msg.sender;
        *0x0 = address(msg.sender);
        *0x20 = 0x2;
        var1 = keccak256(0x0, 0x40);
        var1 = storage[var1];
        var0 = var1;
    }
    else {
        var0 = 0x0;
    }

    return var0;
}

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 == 0x2a0f7696) {
            // Dispatch table entry for 0x2a0f7696 (unknown)
            if (msg.value) { revert(memory[0x00:0x00]); }

            var var1 = 0x0081;
            var var2 = msg.data[0x04:0x24] & 0xffff;
            var1 = func_00CC(var2);
            var temp0 = memory[0x40:0x60];
            memory[temp0:temp0 + 0x20] = var1;
            var temp1 = memory[0x40:0x60];
            return memory[temp1:temp1 + (temp0 + 0x20) - temp1];
        } else if (var0 == 0x5b6b431d) {
            // Dispatch table entry for Withdraw(uint256)
            if (msg.value) { revert(memory[0x00:0x00]); }

            var1 = 0x00c0;
            var2 = msg.data[0x04:0x24];
            Withdraw(var2);
            stop();
        } else if (var0 == 0x9f1b3bad) {
            // Dispatch table entry for Receive()
            var1 = 0x00ca;
            Receive();
            stop();
        } else { revert(memory[0x00:0x00]); }
    }

可看出public的函数有3个,分别是sub_5c(0x2a0f7696), Withdraw(0x5b6b431d)和Receive(0x9f1b3bad)

再看合约的交易日志(交易成功的日志)

按照时间先后顺序日志如下:

1:0x2a0f7696
2:0x2a0f7696c1cb
3:0x2a0f7696000000000000000000000000000000000000000000000000000000000000c1cb
4:0x9f1b3bad
5:0x2a0f7696000000000000000000000000000000000000000000000000000000000000c1cb

对应sub_5c调用了4次,Receive调用了1次

分别查看交易的Parity Trace,可查看输入输出

前四个交易均返回0x0,第5个交易返回0x333443335f6772616e646d615f626f756768745f736f6d655f626974636f696e

查看一下逻辑,前面三个调用均失败,sub_cc有条件(par1 & 0xffff) == (var1 & 0xffff),par1为函数输入值,var1为内存值,若不相等则直接返回0x0, 说明前面的三次调用均不满足这个条件。交易5有返回值,说明经过调用Receive函数后就可以满足条件了。

查看main入口函数,sub_5c函数和Withdraw函数均不接受msg.value,证明是not payable, 但Reveive函数可接受msg.value

Receive函数 主要操作storage[0x1]=storage[0x1]^msg.value;

直接解码交易5的返回结果得到34C3_grandma_bought_some_bitcoin

点击收藏 | 2 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖