区块链智能合约逆向初探

admin 2022年3月11日10:30:09评论91 views字数 10061阅读33分32秒阅读模式

以太坊上的智能合约几乎都是开源的,没有开源的智能合约就无从信任。但有些智能合约没有开源,反编译是研究的重要方式,可通过直接研究EVM的ByteCode。


 如何对合约进行逆向分析,下面结合ctf实例介绍区块链合约逆向如何开展,希望区块链入门者能从中学到知识。


ctf实例1

给了bytecode字节码及交互记录


ByteCode:


0x60806040526004361061006d576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806304618359146100725780631cbeae5e1461009f578063890eba68146100cc578063a2da82ab146100f7578063f0fdf83414610127575b600080fd5b34801561007e57600080fd5b5061009d60048036038101908080359060200190929190505050610154565b005b3480156100ab57600080fd5b506100ca6004803603810190808035906020019092919050505061015e565b005b3480156100d857600080fd5b506100e1610171565b6040518082815260200191505060405180910390f35b34801561010357600080fd5b50610125600480360381019080803560ff169060200190929190505050610177565b005b34801561013357600080fd5b50610152600480360381019080803590602001909291905050506101bb565b005b8060008190555050565b6000548114151561016e57600080fd5b50565b60005481565b60008060009150600090505b60108110156101ab576008829060020a0291508260ff16821891508080600101915050610183565b8160005418600081905550505050565b8060036000540201600081905550505600a165627a7a7230582012c9c1368a7902a818e339b8db79b7130db8795bd2a793898b509dc020d960d20029


交互日志:


log1:func_01770xa2da82ab0000000000000000000000000000000000000000000000000000000000000009
log2: #a()0xf0fdf83400000000000000000000000000000000000000000000000000000000deadbeaf
log3: #func_01770xa2da82ab0000000000000000000000000000000000000000000000000000000000000007
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 MethodsMethod names cached from 4byte.directory.0x04618359 Unknown #func_01540x1cbeae5e winner(uint256)0x890eba68 flag()0xa2da82ab Unknown #func_01770xf0fdf834 a(uint256)
--Internal Methodsfunc_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()


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


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

#输入参数xdef 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=36269314025157789027829875601337027084var=7a=0b=0for i in range(0x10):    a=a*(2**8)^(var&0xff)
x=x^ax=(x-0xdeadbeaf)/3
var=9a=0b=0for 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


再ethereummainnet查看合约地址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:0x2a0f76962:0x2a0f7696c1cb3:0x2a0f7696000000000000000000000000000000000000000000000000000000000000c1cb4:0x9f1b3bad5: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


来源:先知

注:如有侵权请联系删除



区块链智能合约逆向初探

欢迎大家加群一起讨论学习和交流
(此群已满200人,需要添加群主邀请)

区块链智能合约逆向初探

从现在开始 不留余力地努力吧,

最差的结果 也不过是大器晚成。


原文始发于微信公众号(衡阳信安):区块链智能合约逆向初探

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2022年3月11日10:30:09
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   区块链智能合约逆向初探http://cn-sec.com/archives/825702.html

发表评论

匿名网友 填写信息