0x00 前言

下拉最后看演示效果。项目地址

本来这应该是一个很和谐的感恩节假期,本来我可以很悠闲的写完所有作业然后随便看点论文打发时间,本来可以很美好的,假装自己不是个程序员。然鹅,一切都因为一篇论文而起,那是篇今年(2018)顶会的论文,内容居然是以太访智能合约的逆向。于是好奇心就起来了,以太访有逆向工具了,为了追上历史的潮流,NEO是不是也应该有一个逆向工具呢?其实在几个月前我写合约的时候就有这种想NEO自己定义了一套指令集,想要通过这些指令去分析交易简直让人崩溃。于是,所以,因此,一股子冲动上来,我就开始作死的坑自己。

0x01 探索

假期有五天,我的第一天计划是用一天时间来研究这个逆向工具,有头绪就好,不着急动手。然后第二第三第四以及第五天就写作业啊,看论文啊之类的。于是第一天。

我在网上搜了很多,基本上针对以太访的合约逆向工具都是17年左右出来的。比较完整的是Porosity,也是号称第一个以太访反编译工具,于是我下载了这个家伙,然后把反编译之外的文件全部删掉。研究之后发现这个项目其实还是蛮原始的,跟我们熟悉的C#或者Java的反编译工具相比,这个Porosity能做的其实很有限,基本上属于一个辅助工具,毕竟这个工具的作者似乎是打CTF的,所以人家开发工具当然是最适合自己就好了。另外一个号称比较完整甚至号称可以反编译ETH/NEO/EOS/BTC的工具是OCTOPUS,于是我也跑去看了,但是感觉总体完成度不是很高,连指令解析都还处于TODO状态,于是提交了下自己在研究的时候发现的bug之后就转去找别的了。虽然这两个项目对我的直接帮助并不大(可以抄代码)那种,但是研究完这两个项目我的冲动更强烈了。好奇心使我灭亡。

0x02 思考

网上搜了很多,也看了关于栈虚拟机分析的文章,但是依然感觉所有的信息在我的脑子里都是碎片化的,我不知道如何入手,完全没有头绪。

我起初的想法是,因为AVM是通过C#的字节码翻译过来的,如果我把这个对应着翻译回去,岂不是可以直接用C#的反编译工具,这简直是美滋滋。于是我开始研究李总的NEOVM和NEOCompiler源码。年初的时候,也就是过年那会,我其实研究过李总的这个虚拟机和编译器的源码,但是当时对NEO整体还不是很熟悉,啃这个源码就像嚼糠一样,含泪咽下去了,也消化不动。现在将近一年过去,中间又断断续续看过一些,现在重新看,感觉轻松了许多,至少每个文件干嘛的都还记得。研究Compiler的方法就是看源码和打Log,然后根据输出对应源码来理解逻辑,蛋是,很快我就发现这样很蛋疼,因为,Compiler在做转换的时候,一条C#指令码可能对应着多条NEO指令,同时多条C#指令也可能对应着相同的NEO指令。再加上C#本身的指令码晦涩程度远超NEO,于是我很快缴械投降放弃了Compiler。

编译的过程搞不定,至少还可以研究下执行的过程,这个虚拟机总是要对每一条指令进行解析的,如果我能搞懂所有的指令,那么岂不是就可以针对这些指令相应的生成高级语言代码!

0x03 起步

第一天的假期就这么过去了,我失去了很重要的一天,但是我想了想,剩下来还有四天,挤一挤还是可以完成原来的计划的。于是我决定第二天继续搞这个东东。

在第一天的夜里,我躺在床上的时候,突然就想到其实我完全可以不必这么纠结,我完全不需要做一个像C#反编译工具那么牛逼哄哄的工具出来,如果开发一个功能完整的工具有困难,那么我就从最简单的开始。于是我决定先不考虑函数调用,系统调用,合约调用等等复杂操作。从最简单的逻辑代码逆向开始。

于是第二天我终于开始敲代码了,我的第一步是把李总的对AVM解析的TS项目迁移到C#上来。至少让我的项目可以输出点东西。这个工作纯粹是搬砖,把李总的ts代码拿过来改成C#,然后加上文件读写和解析,到真正能运行的时候,宝贵的上午已经过去了。于是我又遇到了瓶颈。

如何使用这些SAM呢?我开始趴在桌子上抓头发,由于我本人没有反编译工具开发的经验,所以能凭借的只有大二学计组时学到的逆向和系统知识以及NEOVM的源码。所以我最后决定,把NEOVM整合到我的反编译工具里,具体来说,就是模拟合约执行的过程。

由于我在这个模拟的过程中需要定义新变量以及追踪变量在堆栈中的执行过程,所以我不能使用NEOVM本身自带的类型也好执行栈也好的所有整套逻辑。大刀阔斧的删掉所有我不能用的文件之后,NEOVM还剩下一个ExecuteEngine。

好吧,完美,于是我重新定义合约的方法类用来存储合约里的函数,重新定义执行栈的元素用来存储合约执行过程中对堆栈读写的变量。再然后就是对NEO的指令进行一对一的解析和翻译。

0x04 小成

这个时候其实已经第二天的夜里了,由于我在的城市夜里很不安全,我决定先回家。于是第三天,莫名的力量诱使着我:“只是稍微看一小会,稍微优化一点点,然后就开始做作业。”

为了研究NEO的指令,我首先写一个空的合约,编译之后记录SAM, 然后再定义一个简单变量并赋值,再记录SAM,如此反复,然后反复对比不同版本的合约和相应的SAM,再对照着NEOVM的解析代码来仔细研究每一个指令的执行原理。同理研究函数调用过程。

等到对这些指令大概熟悉之后,开始对每一个指令进行解析翻译。

由于在NEO中,每一个函数都是以指令RET作为结束标志符,而且这个指令不会在别的地方出现,因此我以RET为标记来获取每一个函数的指令并保存在NEOMethod对象中。函数的名字由sub_ 前缀加上函数首地址,Main函数总是在合约的第一个,所以很容易获取合约入口,Main函数将仍旧以Main命名。

函数调用的指令是CALL,后面跟着目标函数的地址偏移,因此只要计算出目标地址的位置,就可以直接获取到目标地址的名称。此处有个问题就是函数的参数以及返回值,这个问题我还没有想到好的解决方案。

在合约执行过程中,每一个会进入堆栈的变量都会有一个名字,变量起名的规则是由一个variable_count来记录当前函数的变量个数,然后在前面加上v_ 作为变量标记符。此外,由于有些变量可能只会用一次,我对每一个变量的引用次数进行记录,在合约解析完成之后,那些只用了一次的变量将会被移除。

系统调用是️虚拟机提供的那些接口,这些系统调用直接就是接口的名字,因此我们可以通过字符串匹配来知道合约在调用哪一个接口。在反编译工具中,我对NEOVM提供的每一个接口都进行了整理,记录了他们需要的参数数量以及输出的参数数量,因此当解析到SysCall的时候,就可以直接翻译为指定的系统调用并添加输入和输出。

其实以上已经是两天的工作量了,没错,五天的假期已经被我一时冲动消耗掉了四天。希望最后一天我能即写完作业又看得完文档还做的了ppt完得成实验。

项目地址:https://github.com/NewEconoLab/NEODecompiler

欢迎有兴趣的小伙伴和我一起完成这个项目。最后是反编译的演示结果:

合约源码:

public static void Main()
        {
            int a = 2;
            string aa="hello";
            string bb = "world";
            string cc = aa + bb;
           // uint b = Blockchain.GetHeight();
        }

反编译结果:

function Main() {
   int v_0 = 4;
   Array<?> v_1 = new Array<?>(v_0);
   int v_6 = 2;
   int v_7 = 0;
   v_1[v_7] = v_6;
   byte[] v_9= new byte[]("hello");
   int v_10 = 1;
   v_1[v_10] = v_9;
   byte[] v_12= new byte[]("world");
   int v_13 = 2;
   v_1[v_13] = v_12;
   int v_15 = 1;
   v_9 = v_1[v_15];
   int v_16 = 2;
   v_12 = v_1[v_16];
   byte[] v_17= v_9+v_12
   int v_18 = 3;
   v_1[v_18] = v_17;
}
11-27 02:52