以太坊上的智能合约几乎都是开源的,没有开源的智能合约就无从信任。但有些智能合约没有开源,反编译是研究的重要方式,可通过直接研究EVM的ByteCode。
如何对合约进行逆向分析,下面结合ctf实例介绍区块链合约逆向如何开展,希望区块链入门者能从中学到知识。
给了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_0154
和func_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}
如果是线下ctf比赛,无法在线反编译,可以准备jeb,尽管是demo版,也基本够用
直接将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; }
可看出反编译效果不错,很容易理解算法。
题目内容
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