主页 > 华为imtoken无法安装 > 以太坊智能合约 OPCODE 反向调试器

以太坊智能合约 OPCODE 反向调试器

华为imtoken无法安装 2023-07-17 05:09:40

智能合约 以太坊_以太坊智能合约代码_以太坊智能合约转不出去币

作者:Hcamael@知道创宇404区块链安全研究组

时间:2018/09/04

上一篇文章对智能合约的OPCODE的基本数据结构进行了研究和分析。 本文将继续深入研究OPCODE,编写智能合约调试器。

混音调试器

Remix 带有一个非常强大的调试器。 当我的调试器写到一半时,我发现了 Remix 内置调试​​器的强大功能。 本文首先介绍 Remix 的调试器。

可调试范围:

1、在对Remix进行各项操作时(创建合约/调用合约/获取变量值),执行成功后,可以点击下方控制界面的DEBUG按钮进行调试

智能合约 以太坊_以太坊智能合约代码_以太坊智能合约转不出去币

2.Debugger可以调试任何交易,只需要在调试窗口输入对应的交易地址即可

以太坊智能合约转不出去币_以太坊智能合约代码_智能合约 以太坊

3. 可以调试公链、测试链、私链上的任意交易

智能合约 以太坊_以太坊智能合约代码_以太坊智能合约转不出去币

点击Environment设置区块链环境,选择Injected Web3,环境取决于浏览器安装的插件

比如我的浏览器是Chrome,安装的插件是MetaMask

通过MetaMask插件,我可以选择环境是公链、测试链还是私有链

以太坊智能合约转不出去币_智能合约 以太坊_以太坊智能合约代码

以太坊智能合约转不出去币_以太坊智能合约代码_智能合约 以太坊

Environment设置为Web3 Provider时,可以自行添加以太坊区块链的RPC节点,一般用于设置环境为私有链

4. JavaScript的EVM环境调试

见图3,将Environment设置为JavaScript VM,表示使用本地虚拟环境进行调试和测试

调试时可以做什么?

智能合约 以太坊_以太坊智能合约转不出去币_以太坊智能合约代码

Remix的debugger只提供详细的数据查看功能,无法在具体指令中操作STACK/MEM/STORAGE

了解了Remix的调试器的功能后,感觉自己是功亏一篑,重新发明轮子。

后来想了想自己写调试器的初衷。 今天的WCTF有一个话题是以太坊智能合约。 因为是第一次认真逆向EVM的OPCODE,所以不熟练。 一个功能一下午没反过,比赛就结束了。 是的,有点遗憾,如果当时可以动态调试,反向速度可能会更快。

Remix的调试器只能调试已经发生的行为(交易),不能满足我对CTF的需求,所以对于我写的调试器,我改变了定位:没有调试的源码,只有智能合约OPCODE逻辑,或者可以称为离线调试。

编写调试器

我觉得写一个智能合约调试器最核心的部分就是实现一个OPCODE解释器,或者说自己实现一个EVM。

OPCODE解释器的实现分为两部分,1.设计实现数据存储(STACK/MEM/STORAGE统称为数据存储),2.解析OPCODE指令

数据存储

根据OPCODE指令,EVM的堆栈数据结构与计算机相同,先进先出,PUSH和POP操作。 但是EVM栈有更多的SWAP和DUP操作,栈交换和栈复制,如下图,是我用Python实现的EVM栈类:

类堆栈(基础):“”“evm堆栈“”“堆栈:[int] max_value:int def __init__(self):self。 堆栈 = [] 自我。 max_value = 2**256 def push(self, data: int): """ OPCODE: PUSH """ self.stack.append(data % self.max_value) def pop(self) -> (int): "" " OPCODE POP """ return self.stack.pop() @Base.stackcheck def swap(self, n): """ OPCODE: SWAPn(1-16) """ tmp = self.stack[-n-1 ] self.stack[-n-1] = self.stack [-1] self.stack[-1] = tmp @Base.stackcheck def dup(self, n): """ 操作码: DUPn(1-16) """ self.stack.append(self.stack[- n])

和电脑的栈相比,我觉得EVM的栈结构更像Python的List结构

智能合约 以太坊_以太坊智能合约转不出去币_以太坊智能合约代码

计算机的栈在一个地址存储一个字节的数据,值可以精确到一个字节,而EVM的栈是以块为单位存储的。 每个PUSH占一个block,每个POP取出一个block。 每个块最多可存储 32 个字。 段数据,也就是2^256-1,所以上面代码中,对入栈的每条数据进行取余计算,保证入栈数据小于2^256-1

EVM的内存的数据结构和计算机内存几乎一样,一个地址存储一个字节的数据。在EVM中,由于栈的结构,每个块存储的数据最大为256位,所以当OPCODE指令要求的参数长度可以大于256位,内存将被使用

如下图,是我用Python实现的MEM内存类:

类 MEM(Base): """ EVM 内存 """ mem: bytearray max_value: int length: int def __init__(self): self. 内存 = bytearray(0) 自我。 max_value = 2**256 自我。 长度 = 0 自我。 扩展(1)@基地。 memcheck def set(self, key: int, value: int): """ OPCODE: MSTORE """ value %= self. 最大自我。 内存[键:键+0x20] = 值。 to_bytes(0x20,“大”)自我。 长度 += 0x20 @Base。 memcheck def set_byte(self, key: int, value: int): """ OPCODE: MSTORE8 """ self. mem[key] = value & 0xff self.

长度 += 长度@Base。 memcheck def set_length(self, key: int, value: int, length: int): """ OPCODE: XXXXCOPY """ value %= (2**(8*length)) data = value。 to_bytes(长度,“大”)自我。 mem[key: key+length] = 数据自身。 长度 += 长度@Base。 memcheck def get(self, key: int) -> (int): """ OPCODE: MLOAD return uint256 """ return int. from_bytes(self.mem[key: key+0x20], "big", signed=False) @Base. memcheck def get_bytearray(self, key: int) -> (bytearray): """ OPCODE: MLOAD 返回 32 字节数组 """ return self.

内存[键:键+0x20] @Base。 memcheck def get_bytes(self, key: int) -> (bytes): """ OPCODE: MLOAD return 32 bytes """ return bytes(self.mem[key: key+0x20]) @Base. memcheck def get_length(self, key:int , length: int) -> (int): """ return mem int value """ return int. from_bytes(self.mem[key: key+length], "big", signed=False) @Base. memcheck def get_length_bytes(self, key:int , length: int) -> (bytes): """ return mem bytes value """ return bytes(self.mem[key: key+length]) @Base.

memcheck def get_length_bytearray(self, key:int , length: int) -> (bytearray): """ return mem int value """ return self. mem[key: key+length] def extend(self, num: int): """ 扩展内存空间 """ self. 记忆。 扩展(字节数组(256*num))

使用python3中的bytearray类型作为MEM结构,默认初始化256B的内存空间,因为有一个OPCODE是MSIZE:

获取活动内存的大小(以字节为单位)。

所以每次设置内存值时,都需要计算活动内存的大小

内存相关设置的说明分为三类

MSTORE,在内存中存储长度为0x20字节的数据

MSTORE8,在内存中存储1个字节长度的数据

CALLDATACOPY(或其他类似指令),在内存中存储指定字节长度的数据

因此,相应地设置了三种不同的内存存储数据的功能。 类似于获取内存数据。

EVM的STORAGE的数据结构与计算机的磁盘存储结构有很大的不同。 STORAGE 用于存储全局变量。 上一篇分析了全局变量的数据结构,所以在用Python实现时,我将STORAGE定义为一个字典,相关代码如下:

以太坊智能合约转不出去币_以太坊智能合约代码_智能合约 以太坊

类存储(基础):“”“EVM存储”“”存储:{str:int} max:int def __init__(self,data):self.storage = data self.max = 2 ** 256 @Base.storagecheck def set(self, key: str, value: int): self.storage[key] = value % self.max @Base.storagecheck def get(self, key: str) -> (int): 返回 self.storage[key ]

因为在EVM中只有SSTORE和SLOAD是操作STORAGE的相关指令,所以使用python的dict类型作为STORAGE的结构是最合适的

解析 OPCODE 指令

OPCODE 指令的解析不是很困难。 指令只占一个字节,所以EVM指令最多是256条指令(0x00-0xff),但是很多都处于UNUSE状态,所以以后在智能合约中加入新的指令后,调试器也会更新,所以现在写的代码需要有可扩展性。虽然解析指令的难度不大,但还是要靠个人努力。 我们先来看看OPCODE的分类。

在以太坊官方黄皮书中,对OPCODE进行了相应的分类:

0s:Stop and Arithmetic Operations(0x00-0x0f指令类型为STOP指令加算术指令)

10s: Comparison & Bitwise Logic Operations (0x10-0x1f指令是比较指令和按位逻辑指令)

20s:SHA3(目前0x20-0x2f只有一条SHA3指令)

30s:环境信息(0x30-0x3f是获取环境信息的指令)

40s:Block Information(0x40-0x4f是获取块信息的指令)

50s: Stack, Memory, Storage and Flow Operations(0x40-0x4f是获取栈,内存,存储信息和流指令(跳转指令))

60 年代和 70 年代:推送操作(0x60-0x7f 是 32 条 PUSH 指令,PUSH1-PUSH32)

80s:Duplication Operations(0x80-0x8f属于DUP1-DUP16指令)

90s:Exchange Operations(0x90-0x9f属于SWAP1-SWAP16指令)

a0s:Logging Operations(0xa0-0xa4属于LOG0-LOG4指令)

f0s:系统操作(0xf0-0xff属于系统操作指令)

以太坊智能合约转不出去币_以太坊智能合约代码_智能合约 以太坊

首先设计一个字节和指令的映射表:

import typingclass OpCode(typing.NamedTuple): name: str removed: int # 参数个数 args: int # PUSH根据此参数获取opcode后args字节的值作为PUSH_OPCODES = { '00': OpCode( name = 'STOP', removed = 0, args = 0), ......}for i in range(96, 128): _OPCODES[hex(i)[2:]] = OpCode(name='PUSH ' + str(i - 95), removed=0, args=i-95)……#由于编译器优化问题,OPCODE中会有很多不可执行和UNUSE指令,为了防止解析失败,还处理 UNUSE for i in range(0, 256): if not _OPCODES.get(hex(i)[2:].zfill(2)): _OPCODES[hex(i)[2:]。 zfill(2)] = OpCode('UNUSE', 0, 0)

然后就是设计一个解释器类:

class Interpreter: """ EVM Interpreter """ MAX = 2**256 over = 1 store: EVMIO ############## # 0s: Stop and Arithmetic Operations ######## ###### @staticmethod def STOP(): """ OPCODE: 0x00 """ Interpreter.over = 1 print("========程序停止========" ) @staticmethod def ADD(x:int, y:int): """ OPCODE: 0x01 """ r = (x + y) % Interpreter.MAX Interpreter.store.stack.push(r)..... .

在这种设计模式下,在解释响应的OPCODE时,可以直接使用

args = [stack.pop() for _ in OpCode.removed]getattr(Interpreter, OpCode.name)(*args)

OPCODE中有几种特殊指令:

1、获取区块信息的指令,例如:

NUMBER:获取块的编号

该指令是获取当前交易所在区块的区块号(区块高度)。 该指令有几种解决方案:

文章开头提到了自己写的调试器的定位,也正是因为这类指令,才想到调试器的定位。 既然已经打包进了区块,那就意味着有了交易地址。 既然有了交易地址,就可以用Remix的调试器调试了。

所以,我写的调试器有离线调试器的定位,使用上面方法中的前三种,优先级从高到低,手动设置>配置文件设置>默认设置

2、获取环境信息指令智能合约 以太坊,如:

ADDRESS:获取当前执行账户的地址。

获取当前合约的地址,解决方法如下:

获取环境信息的指令,因为调试是OPCODE,没有源码,不需要部署,所以无法通过RPC获取,只能由调试器手动设置

智能合约 以太坊_以太坊智能合约转不出去币_以太坊智能合约代码

3. 日志记录说明

LOG0-LOG4:附加没有主题的日志记录。

添加日志信息到交易回执

> 伦理。 getTransactionReceipt("0xe32b3751a3016e6fa5644e59cd3b5072f33f27f10242c74980409b637dbb3bdc"){ blockHash: "0x04b838576b0c3e44ece7279b3b709e336a58be5786a83a6cf27b4173ce317ad3", blockNumber: 6068600, contractAddress: null, cumulativeGasUsed: 7171992, from: "0x915d631d71efb2b20ad1773728f12f76eeeeee23", gasUsed: 81100, logs: [], logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", status: "0x1 ”,至:“0xd1ceeeefa68a6af0a5f6046132d986066c7f9426”智能合约 以太坊,事务哈希:“0xe32b3751a3016e6fa5644e59cd3b5072f33f27f10242c74980409b637dbb 3 bdc", transactionIndex: 150}

上面是获取一个事务的收据,里面有一个日志列表,用来存放日志信息

既然是调试OPCODE,那么记录日志的操作就没有必要了,因为调试的时候可以看到存储/参数的状态,所以对于这类指令的操作,可以直接输出或者不做任何处理(direct pass)

四、系统操作说明

这类指令主要是和外部调用有关,比如CREATE可以创建一个合约,比如CALL可以调用其他合约,比如销毁自己,把余额全部转移给别人 SELFDESTRUCT

我认为这种指令的唯一解决方案是:调试器手动使用调试器设置指令的返回值

调用此类函数时,我们可以看到详细的参数值,因此我们可以手动创建合约、调用合约等操作

总结

完成一个OPCODE解释器后,一个调试器即使完成了3/4,剩下的工作就是实现你想实现的调试器功能,比如设置断点,查看栈内存存储数据等。

这是接近成品的演示 gif:

以太坊智能合约代码_以太坊智能合约转不出去币_智能合约 以太坊

以太坊智能合约代码_以太坊智能合约转不出去币_智能合约 以太坊

过去的流行

以太坊智能合约代码_智能合约 以太坊_以太坊智能合约转不出去币

智能合约 以太坊_以太坊智能合约代码_以太坊智能合约转不出去币