0%

StudyRecord-逆向分析以太坊智能合约Part1

Pre:

逆向分析以太坊智能合约(part1) 学习记录+Demo测试原文地址:https://arvanaghi.com/blog/reversing-ethereum-smart-contracts/

介绍以太坊虚拟机(Ethereum Virtual Machine,EVM)的工作原理,以及如何对智能合约(smart contract)进行逆向分析。

以太坊虚拟机:

以太坊虚拟机(EVM)是一种基于栈的、准图灵完备(quasi-Turing complete)的虚拟机。

基于栈:

EVM并不依赖寄存器,任何操作都会在栈中完成。操作数、运算符以及函数调用都置于栈中,并且EVM知道如何处理数据、执行智能合约。

以太坊使用Postfix Notation(后缀表示法)来实现基于栈的运行机制。简而言之,操作符最后压入栈,可以作用于先前压入栈的数据。

举个例子:来看一下2 + 2操作,在脑海中,我们知道中间的运算符(+)表示我们想执行2加2这个操作。将+放在两个操作数之间是一种办法,我们也可以将它放在两个操作数后面,即2 2 +,这就是后缀表示法。

准图灵完备:

如果一切可计算的问题都能计算,那么这样的编程语言或者代码执行引擎就可以称为“图灵完备(Turing complete)”。这个概念并不在意解决问题的时间长短,只要理论上该问题能被解决即可。比特币脚本语言不能称为图灵完备语言,因为该语言的应用场景非常有限。

在EVM中,我们可以解决所有问题。但我们还是将其成为“准图灵完备”,这主要是因为成本限制问题。

gas是EVM中的一个可计算单位,可以用来衡量操作所需的成本。当某人在区块链上发起交易时,交易代码以及待执行的任何后续代码都需要在矿工的主机上执行。由于代码需要在矿工的内存中执行,这个过程会消耗矿工主机的成本,如电力成本、内存以及CPU计算成本等。

为了激励矿工来保证交易顺利进行,发起交易的那个人需要声明gas price,或者他们愿意为每个计算单元支付的价格。将这个因素考虑在内后,对于非常复杂的问题,所需的gas量将变得非常庞大,此时由于我们需要为gas定价,因此在以太坊中,从经济角度来考虑的话复杂的交易并不划算。

Bytecode的组成:

在编译合约时,我们可以得到完整的Bytecode字节码,可划分为3个部分:

  • Deployment Bytecode(部署字节码)

    • 执行初始化新合约账户的所有操作,包含:
    • 包含用户实际交易调用这个新合约时需要执行的字节码(即运行时字节码)
    • 合约的构造函数中进行初始化处理的代码
  • Runtime bytecode(运行时字节码)

    • 当新合约被调用时所执行的所有字节码,不包含需要在部署中用来初始化合约的字节码。
  • Auxdata

    • Auxdata 是源代码的密码指纹,用于验证。这只是数据,从未由 EVM 执行。

Greeter.sol link:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
pragma solidity >=0.4.22 <0.6.0;

contract Mortal {
/* Define variable owner of the type address */
address owner;

/* This constructor is executed at initialization and sets the owner of the contract */
constructor() public { owner = msg.sender; }

/* Function to recover the funds on the contract */
function kill() public { if (msg.sender == owner) selfdestruct(msg.sender); }
}

contract Greeter is Mortal {
/* Define variable greeting of the type string */
string greeting;

/* This runs when the contract is executed */
constructor(string memory _greeting) public {
greeting = _greeting;
}

/* Main function */
function greet() public view returns (string memory) {
return greeting;
}
}

solc编译:

1
2
3
4
5
$ solc --bin Greeter.sol

======= Greeter.sol:Greeter =======
Binary:
608060405234801561001057600080fd5b5060405161040e38038061040e8339818101604052602081101561003357600080fd5b810190808051604051939291908464010000000082111561005357600080fd5b8382019150602082018581111561006957600080fd5b825186600182028301116401000000008211171561008657600080fd5b8083526020830192505050908051906020019080838360005b838110156100ba57808201518184015260208101905061009f565b50505050905090810190601f1680156100e75780820380516001836020036101000a031916815260200191505b50604052505050336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff160217905550806001908051906020019061014492919061014b565b50506101f0565b828054600181600116156101000203166002900490600052602060002090601f016020900481019282601f1061018c57805160ff19168380011785556101ba565b828001600101855582156101ba579182015b828111156101b957825182559160200191906001019061019e565b5b5090506101c791906101cb565b5090565b6101ed91905b808211156101e95760008160009055506001016101d1565b5090565b90565b61020f806101ff6000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c806341c0e1b51461003b578063cfae321714610045575b600080fd5b6100436100c8565b005b61004d610138565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561008d578082015181840152602081019050610072565b50505050905090810190601f1680156100ba5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610136573373ffffffffffffffffffffffffffffffffffffffff16ff5b565b606060018054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101d05780601f106101a5576101008083540402835291602001916101d0565b820191906000526020600020905b8154815290600101906020018083116101b357829003601f168201915b505050505090509056fea265627a7a723158206f24f4307507b779edd30cf59bfb6e8ace69f2973e780c4ec5701dbf9a62257f64736f6c63430005110032
1
2
3
4
5
$ solc --bin-runtime Greeter.sol

======= Greeter.sol:Greeter =======
Binary of the runtime part:
608060405234801561001057600080fd5b50600436106100365760003560e01c806341c0e1b51461003b578063cfae321714610045575b600080fd5b6100436100c8565b005b61004d610138565b6040518080602001828103825283818151815260200191508051906020019080838360005b8381101561008d578082015181840152602081019050610072565b50505050905090810190601f1680156100ba5780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610136573373ffffffffffffffffffffffffffffffffffffffff16ff5b565b606060018054600181600116156101000203166002900480601f0160208091040260200160405190810160405280929190818152602001828054600181600116156101000203166002900480156101d05780601f106101a5576101008083540402835291602001916101d0565b820191906000526020600020905b8154815290600101906020018083116101b357829003601f168201915b505050505090509056fea265627a7a723158206f24f4307507b779edd30cf59bfb6e8ace69f2973e780c4ec5701dbf9a62257f64736f6c63430005110032

20220717140120

如上所示,我们可知runtime bytecodecontract bytecode的一个子集。

逆向分析:

我们只逆向runtime bytecode,因为这个过程足以告诉我们合约具体做了哪些工作。

不知道原文代码块的编译器版本,所以没法获得一样的字节码,主要看看分析思路

ethersplay的导入过程:

  • 复制 ascii 十六进制字符串,然后在 Binary Ninja 中创建一个新文件。

  • 右键单击并选择Paste From -> Raw Hex

  • 将此文件另存为test.evm并关闭它。在用

  • 不能直接将solc命令行输出的bytecode直接保存在.evm文件里,会读取不到

QuickView:

20220717141936

与原文的图不一样,问题不大,参考思路,自己推导

Ethersplay插件可以识别runtime bytecode中的所有函数,从逻辑上进行划分。对于这个合约,Ethersplay发现了函数:kill()greet()。后面我们会介绍如何提取这些函数。

第一条指令:

当我们向智能合约发起交易时,首先碰到的是合约的dispatcher(调度器)Dispatcher会处理交易数据,确定我们需要交互的具体函数。

20220717161417

模拟opcode在栈里的操作:

20220717165456

函数调度的前置判断:

20220717165643

模拟opcode在栈里的操作:

20220717171226

为什么EVM需要检查我们提供的calldata大小是否至少为4字节?这里涉及到函数的识别过程。

EVM会通过函数keccak256哈希的前4个字节来识别函数。也就是说,函数原型(函数名以及所需参数)需要交给keccak256哈希函数处理。在这个合约中,我们可以得到如下结果:

1
2
keccak256("greet()") = cfae3217...
keccak256("kill()") = 41c0e1b5...

因此,greet()的函数标识符为cfae3217kill()的函数标识符为41c0e1b5。Dispatcher会检查我们发往合约的calldata(或者消息数据)大小至少为4字节,以确保我们的确想跟某个函数交互。

函数标识符大小始终为4字节,因此如果我们发往智能合约的数据小于4字节,除非定义了回退函数,否则必定无法匹配到可交互的函数,就无法与任何函数交互。

函数调度:

20220717175933

模拟opcode在栈里的操作:

20220717175844

其中重要的操作是:

  • 通过逻辑移位的方式,取出4字节的函数标识符。

  • kill()的函数标识符与 tx的函数标识符比对,相等则跳到 kill函数去

这个过程正是dispatcher的“调度”过程:将calldata函数标识符与智能合约中所有的函数标识符进行对比。

1
2
3
# kill()的函数标识符与 tx的函数标识符比对
PUSH4 #41c0e1b5 // kill()函数
EQ
1
2
3
# greet()的函数标识符与 tx的函数标识符比对
PUSH4 #cfae3217 // greet()函数
EQ

如果我们没有跳转到kill()函数,那么dispatcher依然会采用相同逻辑,将calldata函数标识符与greet()函数标识符进行对比。

Dispatcher会检查智能合约中的每个函数,如果不能找到匹配的函数,则会将我们引导至程序退出代码。

Summary:

以上是对以太坊虚拟机工作原理的简单介绍,画个图总结一下

20220717182929

(6) kill()的函数标识符与 tx的函数标识符比对,跳转到kill函数

Online Solidity Decompiler:

online decompile

20220717183206
20220717183235

有一些提示信息不错,伪代码,反编译块的输入和输出,前期不熟悉的时候可以自己手动模拟stack的操作了,后期可以直接看input/output。

Refs: