EOS 合约逆向教程(EOS Contract reverse engineering)

背景知识首先,EOS的MEM/CPU/NET 体系需要参阅相关内容。是必备的极其基础知识。 其次,EOS的 DPoS 机制也应有所了解,应知道是 21个节点,每个节点通过选举...

背景知识

首先,EOS的MEM/CPU/NET 体系需要参阅相关内容。是必备的极其基础知识。 

其次,EOS的 DPoS 机制也应有所了解,应知道是 21个节点,每个节点通过选举成为bp(block producer),负责验证交易并出块。

EOS 作为一个较新的公链,和之前 ETH相比大概有如下不同(含以上两条)

  1. 出块速度每秒两个
  2. 号称峰值交易处理能力 2000req/s (初期值,现在不清楚了)
  3. 帐号名体系,而非公钥地址+私钥;较复杂的帐号名权限体系
  4. 主币 EOS 依然通过合约实现(ETH主币在实现中,代币通过ERC20体系)
  5. 公链基础架构几乎完全通过合约实现(包括账户信息,主币体系,代币体系)
  6. 采用 webassembly 作为合约运行时虚拟机,公链成为运行webassembly的分布式虚拟机,合约编程语言只要支持webassembly target均可(ETH是自定义的基于堆栈的虚拟机)
  7. 合约部署后可随时修改(ETH无法修改)
  8. 主链规则由于通过合约实现,所以可以随时修改(即链的某些限制、规则是有可能发生改变的)合约通过 ABI 描述文件告诉全网自己支持的操作和参数类型(ETH在合约不开源的情况下,无法提供该信息)

EOS 合约执行流程

一条EOS合约调用(包括普通的 EOS转账,帐号创建,都是合约调用),是如何执行的呢?

首先,用户,或者DApp网站,生成最基础的调用,三要素:

  • 被调用的合约 => code/account
  • 所调用合约的函数,方法,动作 => action/name
  • 函数的参数 => args/data,一般是 json object,也可以是函数参数的 Array。但本质上是JSON序列化后的一串二进制字符 

以上三要素必须和 ABI 描述中对应的 action 一致。

随后,浏览器钱包被调起(或者命令行钱包keosd/cleos),选择授权(Authorization)身份,即账户名@子权限  例如 nvzhuangdalao@active

钱包用来管理不同身份和其私钥。

然后,钱包完成对调用三要素的序列化处理,并进行私钥签名。加入其他必备信息,打包为一个交易,发送给公链API节点,由节点广播到整个公链网络。

bq节点接到请求,验证通过后,执行该笔交易。得到结果,生成区块,广播到全网。

EOS 合约执行的 WASM 虚拟机部分

之前说了,EOS 的合约是 webassembly,那么其中的接口和执行过程是如何呢?

一个空合约如下,即最基础的 EOS 合约编译为 webassembly 后,必须提供 init 和 apply 两个接口。

 extern "C" {

    void init()  {  }

    void apply( uint64_t receiver, uint64_t code, uint64_t action ) {}

}

init() 函数初始化合约的全局变量,内存表,等。

apply()函数是所有合约方法调用的入口。receiver 是合约自己,code 是被调用合约,action是被调用合约的方法。(因为 receiver 可以 !=code, 所以合约可以被通知,感知到其他某一合约的调用。即合约不只在自己被调用时才被调用,在被通知时候也会被调用。)

在apply()函数中,根据不同的方法,调用具体不同的实现。EOS 合约编写中的宏,就是用来自动处理 init, apply 函数的。

这里的参数 u64 类型,其实是通过 name/string 转换机制和12(13) 位字符串互转。大概相当于一个简单的压缩,之后有代码。而且值得一提的是,EOS中的 name, action_name, table_name 均用此机制。所以对照 ETH 的合约函数调用是一个签名hash,EOS的是可逆的。

EOS 合约逆向

所执行的合约代码都是被保存在链上的,EOS也提供了 API去获取某一合约的代码和ABI

bin/cleos -u https://geo.eosasia.one get code --wasm -c out.wasm -a out.abi nvzhuangdalao

 

但是 EOS 目前的实现是有BUG的,所以以上拿到的 wasm文件是错误的

所以换个方法获取:

curl -X POST -d '{"account_name":"nvzhuangdalao","code_as_wasm":true}' https://eos.greymass.com/v1/chain/get_code | LC_ALL=C python3 -c 'print(import("json").loads(import("sys").stdin.read())["wasm"],end="")' > nvzhuangdalao.wasm

即就是需要在无编码或者latin1编码的情况下解析JSON,并取字段。(是的,官方的 chain_plugin 实现是有 BUG 的)

取得 wasm文件后,下载 wabt (The WebAssembly Binary Toolkit)工具 https://github.com/WebAssembly/wabt

执行:

path/to/wabt/wasm2c nvzhuangdalao.wasm >nvzhuangdalao.c 

即得到反编译后的 C 文件。其实几乎是 wasm 的指令集对应文件,可读性极差。

但从 apply 函数的跳转表依然可以找到不同合约 action对应的字函数,并从memory 的 data 部分中不同 error 信息字符串的加载位置中找到特征,获得程序的大致逻辑。

大致逆向后合约文件阅读思路如下:

  • 寻找 eosiolib 中合约运行时的函数调用
  • 利用 C++ demangle 工具寻找导出函数
  • 利用反编译后 init_memory() 函数获得静态字符串常量的内存地址,并寻找特征引用位置
  • 利用 init_table() 函数和 CALL_INDIRECT 宏寻找函数和合约 action 的对应关系
  • 迅速定位 apply 函数,并找到跳转表
  • 利用 name/string 转换工具获得函数名、表名的特征 u64 并全文查找
  • 定位 env->action_data_size(), env->action_data() 调用,一般是合约函数入口
  • 定位 env->db_lowerbound() ,其第三个参数是table,用 name/string 反查工具获得表名,得知具体访问的表
  • 其他技巧还有很多,需要自己摸索

name/string 转换工具

import sys

 

def string_to_name(val):

    s = list(val)

    def char_to_symbol(c):

        if 'a' <= c<= 'z':

            return ord(c) - ord('a') + 6

        if '1' <= c<= '5':

            return ord(c) - ord('1') + 1

        return 0

    name = 0

    i = 0

    while i <len(s) and i < 12:

        name |=(char_to_symbol(s[i]) & 0x1f) << (64 - 5 * (i+1))

        i += 1

    if i == 12:

        name |=char_to_symbol(s[12]) & 0x0f

    return name

 

def name_to_string(val=13949526960272233840):

    charmap =".12345abcdefghijklmnopqrstuvwxyz"

    result = ['.'] *13

 

    for i inrange(12+1):

        c =charmap[val & (0x0f if i == 0 else 0x1f)]

        result[12-i] =c

        val >>=(4 if i == 0 else 5)

    result =''.join(result).rstrip('.')

 

    return result

 

if name == 'main':

    arg = sys.argv[1]

    if arg.isdigit():

       print(name_to_string(int(arg)))

    else:

       print(string_to_name(arg))

其他 EOS 合约可参考资料

由上,通过特殊机制,合约可以不发布ABI, 参考: https://zhuanlan.zhihu.com/p/42903901

合约如何监听转账? https://eosio.stackexchange.com/questions/421/how-to-do-something-when-your-contract-is-an-action-notification-recipient-like

C++demangler https://demangler.com/