In my previous tutorial, we began reversing engineering the Greeter.sol contract. Specifically, we looked at Greeter.sol’s dispatcher, the portion of the contract that takes your transaction data and determines to what function it should send you.

Here’s the Greeter.sol contract again.

contract mortal {/* Define variable owner of the type address */address owner;/* This function is executed at initialization and sets the owner of the contract */function mortal() { owner = msg.sender; }/* Function to recover the funds on the contract */function kill() { if (msg.sender == owner) selfdestruct(owner); }
}contract greeter is mortal {/* Define variable greeting of the type string */string greeting;/* This runs when the contract is executed */function greeter(string _greeting) public {greeting = _greeting;}/* Main function */function greet() constant returns (string) {return greeting;}
}

Let’s examine the kill() method this time.

The dispatcher exists in every smart contract in existance. The function identifier for “kill()” is 0x41c0e1b5, because these are the first 4 bytes of its keccak256 hash:

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

Here is the part of the dispatcher that examines the incoming transaction to our smart contract and determines whether it wants to communicate with the kill() function. Again, for a more thorough breakdown of these instructions, see part 1.

Let’s examine what happens when the dispatcher sends us here.

kill()

The kill() function in the Greeter.sol contract is actually inherited from the mortal contract above it:

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 {...
}

Because greeter is mortal, all of mortal’s functions and members are accessible to greeter. Even though we only placed the bytecode for greeter into Binary Ninja, because of this inheritence, that bytecode contains all of mortal’s functions as well.

The kill() function does the following:

1) Checks to see if the address that sent the transaction matches the address owner member of the contract.

2) If so, kill() calls the built-in selfdestruct function and passes the owner address as an argument.

selfdestruct is actually an opcode, so it is already built in to the EVM. It’s the only way, in theory, you can remove your smart contract from the Ethereum blockchain. If your contract accepts ether, the address you pass as an argument to selfdestruct receives all the ether stored in your contract before the contract code is deleted.

The motiviation for selfdestruct (initially called suicide until EIP6) was to allow people to de-clutter the blockchain by deleting their old or unused contracts. If anyone sends ether to a contract that has been self-destructed, it will be lost forever, since the contract address no longer has any code to transfer ether to another address. You can read more about selfdestruct here.

Disassembling kill()

Let’s disassemble kill() and examine the opcodes.

Being “Payable”

The first instructions are:

CALLVALUE
ISZERO
PUSH2 0x5c
JUMPI

CALLVALUE is the number of wei sent in a transaction and corresponds to the msg.value parameter of a transaction. Wei is the smallest denomination of ether, like a cent is to a dollar, except 1 ether = 1018 wei. For simplicity, I’ll denote the value sent in ether.

CALLVALUE pushes however many ether were sent to the kill() function onto the stack. ISZERO pops this value off and pushes 1 onto the stack if it was 0 (no ether was sent to kill()).

Remember, msg.data corresponds to calldataload, whereas msg.value corresponds to callvalue. An Ethereum transaction to a contract contains both fields. The msg.data field tells the smart contract with what function the transaction wants to interact, and also contains any arguments for that function. The msg.value field can include some ether for that function as well — a totally separate field.

In our case, let’s assume someone did send some ether in their transaction to kill(). This means 0 gets pushed onto the stack by ISZERO. After PUSH2 0x5c, the stack looks like so:

0: 0
1: 0x5c

As explained in part 1, JUMPI is jumpi(label, cond), which means to jump to label if cond is nonzero. In this case, cond is 0, so we don’t jump. This leads us to this branch on the left, which gets us immediately to a REVERT.

Why do we get to a REVERT if someone sent ether to the kill() function? Because the kill() function is not marked as payable in the source code.

function kill() {

When a function prototype does not have the payable modifier at the end of it, it rejects any transaction intended for it that contains ether. If a smart contract author doesn’t explicitly include a function to forward the ether stored in their contract elsewhere, it is lost forever. Requiring this “payable” modifier ensures this happens less frequently.

Optimizer

Solidity has done a great job at serving as an accessible language for such a daunting task as writing smart contracts. However, as it is still relatively new (the same applies to Ethereum in general), the Solidity compiler solc can produce redundant instructions in the compiled bytecode.

Take, for example, this set of instructions in our kill() function:

These three instructions are PUSH1 0x0DUP1, and SWAP1. What this does is pushes 0x0 onto the stack:

0: 0x0

… duplicates it:

0: 0x0
1: 0x0

… then swaps it, so the two 0x0s on the stack have flipped.

0: 0x0
1: 0x0

These kinks are still being worked out, but luckily the solc compiler has an optimizer flag which does a good job getting rid of some of these redundancies. You can read more about it here.

In our case, we could generate optimized bytecode with the following command:

solc --bin-runtime --optimize --optimize-rounds 200 Greeter.sol

Placing that new bytecode into Binary Ninja, we get the following output:

You’ll notice the payable logic we examined is still the same, but the number of operations have dramatically decreased!

We’ll continue our analysis with this optimized bytecode.

Breaking down kill()

As we’ve already covered the payable logic, we will proceed with the instructions immediately following it in kill():

The first instruction we see is PUSH2 0x65. This will actually stay on the stack until the very end of kill(). You can tell this is the case ahead of time, because if you look at the very bottom, there is a JUMP instruction at address 0x131.

We know that JUMP requires an argument to tell the EVM where to jump, so there must be something still on the stack. We also see that this JUMP instruction immediately leads us to address 0x65. Thus, we can conclude the 0x65 we just pushed onto the stack will be used as an argument for this JUMP instruction at the very end of this function.

The next instruction, PUSH2 0xf1, is just an argument for the JUMP immediately after it. After the JUMP takes place, the stack once again only contains 0x65

Next, we have the first major part of kill():

After JUMPDEST, which serves as a placeholder saying where a JUMP landed, the first instructions are PUSH1 0x0 and then SLOAD. We know that SLOAD is short for storage load, which loads from an index in storage and then pushes it onto the stack.

Instruction Result
sload(p) storage[p]

In this case, 0 is the argument passed to it (since it is directly above it on the stack), so SLOAD pushes storage[0]on the stack. In our contract, this is “address owner” member of our contract!

0: 0x65
1: contract owner's address

The next instruction is CALLER, which pushes the address of the call sender (or the person/contract who sent the transaction).

0: 0x65
1: contract owner's address
2: caller address

After PUSH20 0xffffff...SWAP1DUP2, we get:

0: 0x65
1: contract owner's address
2: 0xffffff... (20 bytes long)
3: caller address
4: 0xffffff... (20 bytes long)

The next instruction is AND. When ANDing 0xffffff… (20 bytes long) with the caller address, nothing changes. This instruction just makes sure the proper bits of the stack are set. AND pops these two values off the stack and then pushes this address onto the stack.

0: 0x65
1: contract owner's address
2: 0xffffff... (20 bytes long)
3: caller address

The next instructions are SWAP2 and then AND, which uses the AND operation on the contract owner’s address. Again, the result of this AND is pushed to the top of the stack, which is the unchanged contratct owner’s address. After these instructions, the stack looks as follows:

0: 0x65
1: caller address
2: contract owner's address

The next instruction is EQ, which checks if the top two stack items are equal, in which case it pushes 1, and otherwise pushes 0. In this case, EQ checks if the caller address is equal to the contract owner’s address.

Does this sound familiar? It should. This was the if (msg.sender == owner) line of the kill() function!

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

The next instruction is ISZERO, which will check if the EQ evaluated to 0 or 1. If it evaluated to 0, it means the message sender was not the contract’s owner, and ISZERO evaluates to true. If ISZERO evaluates to true, it pushes 1 onto the stack, and ultimately tells the JUMP instruction to skip the next block and go to 0x130, which will soon kick you out of the contract.

Let’s assume the address that sent this transaction did match the contract’s “owner” address. Execution would continue at the PUSH1 0x0. After this instruction, the stack looks as follows:

0: 0x65
1: 0

Again, we have SLOAD, which again took 0 as its argument and therefor pushes the contract owner’s address onto the stack. After the familiar PUSH20 0xffffff... and AND instructions, our stack contains:

0: 0x65
1: contract owner's address

The last instruction in this block is SELFDESTRUCT, which treats the topmost item on the stack as the destination address for all the contract’s stored ether, and then deletes all the contract’s code. After SELFDESTRUCT pops off the contract owner’s address, all that’s left on our stack is 0x65, which again is used as the argument to that final JUMPinstruction that leads to a STOP.

Our contract’s code has now been deleted, and all the ether stored in the contract has been sent to owner. Well done!

https://arvanaghi.com/blog/reversing-ethereum-smart-contracts-pt2/

Reversing Ethereum Smart Contracts: Part 2相关推荐

  1. Understanding Ethereum Smart Contracts

    You might have heard the term "smart contract," and you might even know that they are &quo ...

  2. 以太坊智能合约生命周期(Ethereum smart contracts lifecycle)

    合约对象初始化 上一节中我们提到Solidity编写合约和面向对象编程语言非常相似,我们可以通过构造函数(constructor)来初始化合约对象.构造函数就是方法名和合约名字相同的函数,创建合约时会 ...

  3. How to Secure Your Smart Contracts: 6 Solidity Vulnerabilities and how to avoid them (Part 2)

    While Part 1 discussed some more high profile or obvious vulnerabilities, this post will be about vu ...

  4. 阅读论文Formal verification of smart contracts based on users and blockchain behaviors models

    1 题目(Formal verification of smart contracts based on users and blockchain behaviors models) 1.1 作者.出 ...

  5. Part 2 — Making Sense of Smart Contracts

    The term "smart contract" has no clear and settled definition. The idea has long been hype ...

  6. How to Secure Your Smart Contracts: 6 Solidity Vulnerabilities and how to avoid them (Part 1)

    In a previous post we discussed the future of Ethereum's scalability by analyzing the concepts prese ...

  7. Setting up Ethereum smart contract development using Parity on Ubuntu

    Ethereum represents one of the most interesting technological developments in the past few years, ta ...

  8. Using APIs in Your Ethereum Smart Contract with Oraclize

    原文地址:https://medium.com/coinmonks/using-apis-in-your-ethereum-smart-contract-with-oraclize-956564342 ...

  9. Plasma: Scalable Autonomous Smart Contracts 翻译

    Plasma 可扩容的智能合约 摘要 Plasma is a proposed framework for incentivized and enforced execution of smart c ...

最新文章

  1. VC操作Excel文件编程相关内容总结
  2. 各种编程语言功能综合简要介绍(C,C++,JAVA,PHP,PYTHON,易语言)
  3. python简单入门代码-Python入门 | IDLE的介绍和使用方法
  4. C++:位操作基础篇之位操作全面总结
  5. 现代密码学2.2、2.3--由“一次一密”引出具有完美安全的密码方案共同缺点
  6. 计算机与操作系统简介
  7. matlab如何将两张图画在一起,如何在MATLAB里面将两个图画在一起
  8. 3-2 文件夹类Directory的常用方法(2)
  9. WSDM 2022 | 基于元学习的多场景多任务商家建模
  10. 高性能对象存储MinIO学习
  11. 在网络虚拟化之前部署NFV将使运营商网络面临风险
  12. atom对比 vscode_VS Code、ATOM这些开源文本编辑器的代码实现中有哪些奇技淫巧?...
  13. maven项目依赖被改为文件夹时如何改回lib
  14. ubuntu静态IP设置
  15. linux怎么安装scp服务,linux下ssh安装与scp命令使用详解
  16. java给界面添加滚动条_Java Swing学习笔记:要求会默写或熟练的,GUI,控件,设置列或行,加滚动条,新界面...
  17. 被苹果“先捧后杀”的操作系统
  18. 使用SmtpClient发邮件时,返回“不允许使用邮箱名称”和 “邮箱不可用”的错误信息...
  19. JAVA常用的工具类
  20. VMware虚拟机中Ubuntu16.04系统下通过MVS运行海康威视工业相机

热门文章

  1. Redis常用命令入门5:有序集合类型
  2. 从零学React Native之07View
  3. 同域下跨文档通信iframe和window.open
  4. self.view = nil 和[self.view release]的区别
  5. [英]Promises Don't Come Easy
  6. noclobber属性
  7. CentOS下的rm命令改造成移动文件至回收站
  8. [云炬Mysql数据库笔记] 第2章 数据库设计
  9. 《秋暮登北楼》王武陵
  10. <马哲>生产方式是社会发展的决定力量2017-12-27