0%

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

Pre:

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

在part1中,我们初步逆向分析了Greeter.sol合约。我们仔细研究了Greeter.soldispatcher。作为合约的一部分,dispatch可以接收交易数据,决定应该发送哪个函数。

Greeter.sol:

part1原文里的Greeter.sol是稍微简单一些的,但我是直接去github找的代码,是跟part2里的一样的

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

这次让我们分析一下kill()方法。

每份智能合约中都存在dispatcherkill()的函数标识符为0x41c0e1b5,这是因为该ID是kill()方法 keccak256哈希的前4个字节:

1
keccak256("kill()") = 41c0e1b5...

Dispatcher会检查发往合约的交易数据,决定是否要与kill()函数进行通信。大家可以回顾之前那篇文章,详细了解我们分解过的那些指令。回顾一下。
20220717224554

这里我们分析下当dispatcher把我们带到这个函数时会发生什么情况。

kill():

Greeter.sol中的kill()函数实际上继承自上一层的mortal合约:

1
2
3
4
5
6
7
8
9
10
11
12
13
contract mortal {
/* Define variable owner of the type address */
address owner;

...

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

contract greeter is mortal {
...
}

由于greeter is mortal,因此greeter可以访问mortal的所有函数以及成员。即便我们只是把greeter的字节码加载到Binary Ninja中,由于存在这种继承关系,该字节码中也会包含mortal的所有函数。

kill()函数有以下执行逻辑:

1、检查发送交易的地址是否与合约的address owner成员相匹配。
2、如果相匹配,kill()就会调用内置的selfdestruct函数,将owner地址以参数形式传入。

selfdestruct实际上是一种操作码(opcode),因此其实已经内置在EVM(以太坊虚拟机)中。理论上讲,这是我们从以太坊区块链上删除智能合约的唯一方法。如果你的合约接收以太币(ether),那么你以参数形式传递给selfdestruct的那个地址会在合约代码被删除前接收存储在你合约中的所有以太币。

selfdestruct(EIP6之前称为suicide)的功能是允许人们通过删除旧的或者未使用的合约来清理区块链。如果有人将以太币发送给已经销毁的合约,那么这些以太币将永远丢失,因为合约地址已经不再具备将以太币转移到另一个地址的任何代码。大家可以访问此链接了解关于selfdestruct的更多信息。

反汇编kill()函数:

接下来让我们反汇编kill(),检查相关操作码。

20220717225237

与原文的图不同,以我自己反编译出来的图为准,参考思路,自己推导

优化:

作为一门高级语言,在编写智能合约这样艰巨的任务方面Solidity已经表现得非常不错。然而,由于这门语言仍属于较新颖的一门语言(对于以太坊来说也是如此),因此Solidity编译器solc在编译出来的字节码中仍然会产生冗余的指令。

solc编译器有一个optimizer标志,可以很好地解决这些冗余问题。

1
solc --bin-runtime --optimize --optimize-runs 200 Greeter.sol
1
2
3
4
5
6
7
8
9
$ solc --bin-runtime --optimize --optimize-runs 200 Greeter.sol

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

======= Greeter.sol:Mortal =======
Binary of the runtime part:
6080604052348015600f57600080fd5b506004361060285760003560e01c806341c0e1b514602d575b600080fd5b60336035565b005b6000546001600160a01b0316331415604a5733ff5b56fea265627a7a72315820195b62087d88b621dfcba99834f4c148a593ab135f2a30aea71437ef0cc9491e64736f6c63430005110032

20220717225858

20220717230328

优化后的opcode确实短一些,相应的图也简洁一些了。我们会继续分析经过优化后的字节码。

进入kill()函数:

继续分析kill()的指令:
20220717230520

20220717233753

模拟栈的操作过程中,暂时搞不清楚左位移怎么计算,暂时先跳过。直接看online decompile的输出好了

1
2
3
4
5
6
7
// Inputs[2]
// {
// @00C5 storage[0x00] // contract owner's address
// @00CF msg.sender
// }

// Block ends with conditional jump to 0x00d8, if !(msg.sender == (0x01 << 0xa0) - 0x01 & storage[0x00])

EQ那里会检查 调用者地址 是否等于 合约所有者的地址。

其实这对应于kill()函数中if (msg.sender == owner)这条语句。

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

20220717234724

20220717234850

1
2
CALLER  // msg.sender压入栈顶,经过前面的判断也就是owner了
SELFDESTRUCT // destroys the contract and sends all funds to addr.

这个指令块的最后一条指令是SELFDESTRUCT,该指令会将栈顶元素当成存储以太币的所有合约的目的地址,然后删除所有合约的代码。现在我们的合约代码已经被删除,存储在合约中的所有以太币已经发送到owner。Over…

Summary:

20220717235727

原文没有给出编译器的版本,我用的0.5.17,比较新升级过的编译器应该是采用了EIP-145的建议:

  • EVM 缺少按 位移位运算符,但支持其他逻辑和算术运算符。

  • 移位操作可以通过算术运算符实现,但成本更高,并且需要主机更多的处理时间。

  • 实现SHL和SHR使用算术每 35 个 gas,而建议的指令需要 3 个 gas。

所以,新编译器的结果少了很多指令,与原文的汇编代码和图都有很大的出入。但思路是差不多的,可以自己参考推导,模拟栈的操作。

evm的很多优化升级都是围绕着gas进行的,不断追求地去降低交易成本。每个opcode都有它对应的gas消耗量,新版编译器(带优化参数)能编译出更短的opcode,可大大降低gas。每个函数都降低一些gas,那成千上万的交易就会省下很多gas.

Refs: