如何黑盒破解 Libra 的序列化算法
Libra中国
2019-07-30
热度16701

如何巧妙地实现Libra浏览器。

前言

在六月底的时候,本着对 Facebook Libra 使用技术的好奇,以及想练习一下 Go 语言,就打算实现一个 Libra 的区块链浏览器。

但在解决用户账号序列化的时候,遇到了一个问题。研究了好久,最后用了一个很有趣的小技巧,感觉可以拿出来和大家分享下。

不过要先从如何写一个 Libra 的浏览器开始,对 Libra 没兴趣的同学可以跳过下一章节。

Libra 浏览器实现方案

如果大家对 BTC / ETH / EOS 熟悉的话,应该对区块链浏览器也不陌生,毕竟他们是查询账号、转账记录、合约执行情况、CPU、RAM 等剩余情况等的主要途径。

成熟的公链都有相关的配套设施,以及公链也会提供相关的 API 给我们,如 EOSX.IO。

但是 Libra 是没有的,这一切都要自己探索。

如果大家看过 Libra 的官方 Guide,会发现 repo 里面是带了一个 client 的,它能和测试网进行交互。

简单扫一下 client 的相关代码 (RUST 语言写的,看着好吃力),就会发现它在 types/src/ 目录下有个 proto 的文件夹,里面放满了 protobuf 的文件。

拿着关键字去代码中 grep,就发现:

  • 测试网地址:

    ac.testnet.libra.org:8000

  • 通信协议用的 gRPC
  • 核心 API 是 getwithproof.proto 中的 UpdateToLatestLedgerRequest
  • 剩下的对着 proto 的定义,创建请求传参数即可,但是当我们序列化 Account Balance 的时候遇到了问题。

    所有数据全被压缩在了这个 blob 里面,我们从链里读出来的全是形如下面这样的二进制。

    但 Client 能把这段数据正确序列化的。

    010000002100000001217da6c6b3e19f1825cfb2676daecce3bf3de03cf26647c78df00b371b25cc9744000000 20000000b3a5493f988054966440aa7540d83d14ff2e6296a8a6a747d29019ec74d3b072404f8100170a000045 0000000000000010040000000000001004000000000000

    猜 Account 二进制数据

    翻源码

    我们知道,Libra 是开源的,那么我们直接去看源码不就是了,拿着关键字 grep,就发现了这么一个段代码:

    等下,account_btree ???

    这个 btree 是我知道的那个 B 树么,ok 我们当没看见,继续搜 make_from,翻出来了这样一个东西:

    这个 Btree 还真是我们认识的那个 btree,但假装没看见,继续翻。

    最后跟进 SimpleDeserializer 里面看之后就再也不想看了,本质上就是 Facebook 定义了一套通用规则,把一个字典拍成二进制。

    我是来练习 Golang 的,不是来练习 Rust 语言翻译的,于是我就开始找其他方案,铺垫了这么多正片终于来了。

    猜数据

    我当时受到了不小的打击,就试图忘掉刚才看到的一切,想想有没有另辟蹊径的解决方法。

    YY 二进制格式

    首先,我们可以 YY 一下 FB 为什么这么设计,把一个字典写成二进制有这么多现成方案不用,比如直接塞一个 proto 进去,或者拍成 json 转成二进制进去。如果是我来想的话,一是觉得 JSON 太冗余,二是觉得 proto 又会导致合约运行的虚拟机变得更复杂。

    基于这两点考虑,以及同时还要考虑兼容性,对于任何字典都生效。那么我们应该像一个二进制文件一样设计这段二进制,即应有两部分组成:

  • 索引,其中包含数据格式与每段数据的位置。
  • 数据本身 one by one 的列在索引后方。
  • YY 二进制索引格式

    对于索引的设计,我又 YY 出来了两种:

  • 一个标记字典格式的东西,然后再记录每段数据的起始点和终止点,对于可变长度的数据更加友好,方便 seek。
  • 仅有一个标记字典格式的东西,不考虑 seek 的 index,直接扫一遍遇到 N 个零就代表一段数据。
  • 但反过头来看我们能从这里获得什么数据?

    我们经过测试知道 auth_key 不设置的前提下就是公钥(256位定长),后面是四个 uint64。那么这个索引无论采用我的 YY 的方案 1 还是方案 2,均为固定长度的。

    验证

    首先,我们用 CLI 创建了两个账号:

    cca29e36f3fd8bb8f93c3a9fbd4f05ef593c5b925de6719767d1938e2b937209 5420e20432a3da8a7f0c8ee6b2dda3115985d55163d62c4b7d0f9656a17ffdd4

    然后第一个账号 1022 libra,第二个账号 72 libra

    其次获取了两个账号的 blob:

    010000002100000001217da6c6b3e19f1825cfb2676daecce3bf3de03cf26647c78df00b371b25cc97440000002 0000000cca29e36f3fd8bb8f93c3a9fbd4f05ef593c5b925de6719767d1938e2b937209807bea3c000000000200 00000000000000000000000000000000000000000000

    010000002100000001217da6c6b3e19f1825cfb2676daecce3bf3de03cf26647c78df00b371b25cc97440000002 00000005420e20432a3da8a7f0c8ee6b2dda3115985d55163d62c4b7d0f9656a17ffdd400a24a04000000000600 00000000000000000000000000000000000000000000

    通过观察法,我们可以发现,前缀是一样的,验证了我们的猜想,均为:

    010000002100000001217da6c6b3e19f1825cfb2676daecce3bf3de03cf26647c78df00b371b25cc974400000020 000000

    然后跟着的就是 public_key,我们可以把他们都裁掉了,再继续对比。

    807bea3c 00000000 02000000 00000000 00000000 00000000 00000000 00000000

    00a24a04 00000000 06000000 00000000 00000000 00000000 00000000 00000000

    通过观察数据,差异最大的 就是第一位了,而我们的测试数据里面差别最大的是 balance。

    但我们第一条数据是 1022,而他这个都快成天文数字了,真的没问题么?

    这个时候,我们来回忆一下数据在系统内的表示方法:

  • uint64 无符号64位整数,也就是 2^64,做一下变换 (2^4)^16 即 16^16 换句话说 16个16进制数。

    即807bea3c 00000000 就是 balance 。

  • 小端规则,高位在后,低位在前。
  • 1byte=8bit 对于十六进制就是两位为一字节。
  • 所以 807bea3c 00000000 正确的写法是 000000003cae7b80,即 0x3CEA7B80 = 1022000000,所以 fb 预留了6个0当做小数位,和 BTC 是一样的。

    根据前几项的规律,可以发现 Libra 的序列化是按照声明的顺序,所以以此类推我们可以解析后面的全部内容了。

    后记

    现在 librablock.io 的前后端均已经完全开源在 Github 上了,欢迎大家提 issue,提 pr,可以点击阅读原文跳转。

    访问:https://github.com/libra-china-org/librablock-frontend

    特别是前端,我第一次用 React.js 还不太会写,手机端适配还处于 Todo 状态。

    最后,我和我的同事们搞了个关于 Libra 的微信讨论群,有兴趣的同学可以在我们的公众号里联系小助手加群。

    本内容旨在传递行业动态,不构成投资建议或承诺。
    为你推荐

    商务合作:TG:@Lottie96