合约反汇编,给智能合约把个脉 | Neo专栏

前言

合约开发安全性

在开发智能合约的时候,我们通常会选择一种高级语言来编写合约逻辑代码,比如Solidity开发以太坊合约、C++开发EOS合约、C#开发Neo合约等。

但由高级语言编写的智能合约代码不能直接在区块链上执行,一般需要特定的编译器翻译成能被特定指令集下智能合约虚拟机识别的脚本。

作为开发者,高级语言的智能合约很容易理解分析,但编译后的脚本会有些难理解。比如,Neo官方提供一个用C#语言开发的Domain合约,很容易读懂,但经过编译器编译后的脚本却很难理解。

以编译器编译后的一部分脚本为例:

55c56b6c766b00527ac46c766b51527ac4616a00c36a53527ac46a53c36a52527ac46a52c36499006a52c3057

由于大部分开发者无法直接读懂脚本,很难发现一些恶意攻击,如:合约编译过程中被编译器插入了恶意代码(苹果公司的XCode工具也受过这种攻击)、合约在钱包程序里进行复制与发布的过程中,其脚本有可能被黑客攻击。事实上,通过读取粘贴板进而攻击区块链账户的案例屡⻅不鲜。

因此合约的逆向分析,直接从脚本出发对合约的功能进行功能分析和安全分析对于合约安全来说至关重要。

安全研究

在安全领域,对可执行程序进行逆向分析是研究目标程序安全性的一种重要方式。尤其是在没有源代码的情况下,通过对可执行程序进行逆向分析,可以更清晰地了解目标程序的内部逻辑,以便及时发现可能出现的漏洞,避免漏洞被恶意利用。

目前合约逆向的主流还是针对于以太坊平台,在该平台上已经有很多相对成熟的合约逆向工具,比如Ethersplay,Porosity 和 EtherVM。甚至在流行的逆向分析工具IDA里还有以太坊合约的逆向插件。当然,也有号称可以对Neo平台合约进行逆向的工具OCTOPUS,但是工具也是止步于prototype阶段。

此外对于安全开发者来说,合约逆向是研究智能合约安全的一个重要途径。在安全领域通行的夺旗赛(CTF)类安全竞技比赛中,针对以太坊合约的逆向分析已经成为一项正式的赛事题目,吸引一大批安全研究员进入合约安全的研究领域。即使在科研学术圈,通过对智能合约进行逆向分析,以大规模扫描线上合约可能存在的漏洞也是一个研究热点。

2018年的顶会论文Enter the Hydra就提到,可以在运行时监测攻击、分析漏洞;TEETHER,一个漏洞扫描工具,通过自动化分析合约脚本成功发现了815个漏洞合约;Erays,一款以太坊合约逆向工具,可以将合约脚本逆向为伪高级语言代码。此外较近的顶会成果还有SODA,一款在线合约漏洞检测框架。

在安全科研圈,智能合约安全方兴未艾。

背景

合约逆向主要分为两步,第一步也将是本文主要介绍的过程,即将机器指令翻译为开发者可以理解的汇编指令,迁移到Neo的合约脚本,也就是将我们通过Neo编译器编译出的AVM智能合约脚本翻译成由NeoOpCode为主体的ASM代码。

第二步是将ASM代码在反编译成我们更易理解的高级编程语言,比如C#或者python,这部分内容我将会在下一篇文章中进行介绍。

分析

本文所使用的指令集为Neo2.0版本,更新的指令集版本将在后续进行更新。

在考虑对Neo合约脚本进行反汇编的时候,首先需要对脚本结构和数据类型进行解析。

Neo合约的的指令和OpCode存在一一对应关系,每个指令都由一个字节进行编码。Neo合约脚本本身是一串hex字符串,里面既包含着合约执行需要的指令,而一个字节的hex占据两个字符。所以在解析指令时,需要从当前位置读取两个字符,并转换成16进制的整型,从而获取和OpCode的对应关系。

除了指令外,Neo合约脚本里还存在着大量的数据,比如地址位置,内置管理员账户,调用合约地址,调用系统函数名,逻辑判断的判断条件等。这些数据都掺杂 在指令之间,在合约执行过程中,通过解析指令来读取后续的数据信息。

归类

合约脚本存在两种类型的数据:指令和纯数据。在脚本的翻译过程中,我们需要区分数据和指令,才能对不同类型的数据进行针对性处理。通过对Neo虚拟机执行逻辑分析,可以了解到Neo的虚拟机是在识别到指定类型的指令之后,根据指令的不同,直接从指令之后读取相应⻓度的脚本作为数据,也就是说,所有的数据,都是紧跟在当前指令之后的。基于此,我们只需要把指令分为读取数据和不读取数据的两大类就可以了。

根据NeoVM,可读取数据的指令有:

在这里我们不需要关心指令具体的作用,只需要知道它是否读取数据以及读取多少数据就足够我们对脚本进行反汇编。至于指令具体的功能,只在我们进行反编译时才起作用。除了以上列出的指令之外,剩下的指令都可以直接翻译为OpCode。

反汇编

通过以上分析,我将先解析脚本,再存储解析结果,最后输出反汇编流程。

为了存储反汇编结果,我先定义一个NeoCode的类,这个类用于存储具体的指令,如在脚本中的位置信息,是否读取数据、读取的数据、数据类型以及部分用于后续反编译时会需要的字段。

数据的类型是根据具体指令而判断的,比如若指令是SYSCALL,那么数据就是字符串,输出时就需要从hex转成string进行输出:

如果是JMP等的跳转指令,则需转换成整型:

如果是APPCALL等的跳转指令,则直接输出hex字符串:

在对脚本进行反汇编的过程中,每识别到一个指令,就新创建一个NeoCode对象,再根据当前上下文对这个NeoCode进行初始化。

反汇编的结果会存储在Dictionary<int, NeoCode>对象里。在输出反汇编结果时,再遍历Dictionary对象,然后调用每个NeoCode内置的toString方法进行输出:

展示

以上过程已经详细解释了Neo合约反汇编的原理和流程,接下来我们再通过一个案例来具体演示为什么需要合约逆向。

此处有一个简单的合约,合约逻辑回永远返回True,合约源码:

编译后得到的合约脚本是:

单看这个脚本,既看不出里面的逻辑结构,也不理解它所展现的意思。如果此时黑客在其中改了一个指令,恐怕也很难发现:

这里我们改掉的是第9和10位,从对应PUSH1的0x51指令改为对应PUSH0的0x00指令。这仅仅是改变一个指令,涉及两个字符,但对应的合约已经变成了完全相反的合约:

如果我们将合约脚本反汇编,将会得到更加直观的汇编代码。通过阅读汇编代码,我们可以更加容易地发现脚本中的异常:

通过逆向来避免合约代码被篡改,只是逆向的一个应用场景。还可以通过在多台设备上编译,进行结果对比。其实逆向最重要的作用还是帮助安全人员和开发人员更好的理解合约执行原理,漏洞的发生原理以及养成安全的合约开发习惯。

当然,反汇编只是逆向合约的第一步,即便汇编代码相对于脚本代码更加直观,可读性大大增强,但是想理解合约的逻辑还有很大难度。

所以之后的文章还将介绍Neo智能合约的反编译,将汇编代码反编译为可读性更高的高级语言。

发表评论

Top