{
"title": "通过 Bytecode 计算合约相似度",
"tags": [
"post",
"data-science",
"web3"
],
"summary": "最近 Forta Foundation 让我帮他们做一个监控机器人,要求给定一个集合的恶意合约(Malicious Contracts),判断当前创造的合约是否与恶意合约集合中的任意一个合约相似。这几天疯狂看了十几篇论文,最终终于比较圆满的完成了这个需求…",
"sources": [
"xlog"
],
"external_urls": [
"https://xlog.soptq.me/tong-guo-Bytecode-ji-suan-he-yue-xiang-si-du"
],
"date_published": "2023-03-19T13:00:00.000Z",
"content": "最近 Forta Foundation 让我帮他们做一个监控机器人,要求给定一个集合的恶意合约(Malicious Contracts),判断当前创造的合约是否与恶意合约集合中的任意一个合约相似。这几天疯狂看了十几篇论文,最终终于比较圆满的完成了这个需求。整个任务感觉非常有意思和挑战性,最终项目在[这里](https://github.com/Soptq/forta-contract-simil)。\n\n### Forta Network 是什么\n\n\n![Forta Network](ipfs://bafybeiabi3ssbirfhhaubw5qcrn2mg6b2h2zoihsyhhqcbmq3lsbah6cfe)\n\n\n[Forta](https://forta.org/) 的官网简介如下:\n\n> The leading decentralized security & operational monitoring network for wallets, developers, and investors\n\n简单的说,Forta Network 是一个用于安全监控的分布式计算平台。开发者可以首先按照需求开发 Forta Bot,Forta Bot 里可以处理区块链上的每一条交易。然后开发者可以把 Forta Bot 上传到 Forta Network,就可以 24 小时处理区块链信息了。\n\n比如说,开发者可以编写一个 Forta Bot 来实时监控算法稳定币是否脱锚。如果脱锚,则发出警告。还比如开发者可以编写一个 Forta Bot 来监控某个项目的合约是否变更了所有者,可以用于提前探测攻击行为。\n\n### 核心困难\n\n由于大部分上传到区块链的合约都是不开源的,所以我们不能直接对合约的源代码进行语义分析。相反,我们只能对合约的 Bytecode 进行分析。Bytecode 失去了源代码中的大量语义信息,所以不管是学术上还是工业上,对合约 Bytecode 的相似度检测都罕被探索。\n\n### 解决方案\n\n我们最终使用函数级别的相似度检测。总的来说,我们的方法的步骤如下:\n1. 反编译合约 Bytecode 到 OPcode 和 CFG。\n2. 遍历 CFG,提取合约的每一个函数,并按函数中 Basicblock 的顺序提取每一个函数中所有指令。\n3. 将一个函数的所有指令向量化到一个稠密向量。\n4. 通过函数间的相似度计算合约间的相似度。\n\n#### 反编译\n\n众所周知,由于在编译的过程中程序不可避免的损失了大量的信息,完全靠谱的反编译是**不存在的**。但是,仅仅讲合约从 Bytecode 反编译到 OPcode 是完全可能且靠谱的。这就像在 x86 上把二进制翻译成汇编语言一样,直接照着说明书一句一句翻译就好了。所以我们反编译的第一步就是按照[ EVM 文档](https://ethereum.org/en/developers/docs/evm/opcodes/)将合约的 Bytecode 翻译成 Opcode。\n\n之后,有了 Opcode 后,我们虽然很难直接从 Opcode 知道这个合约要干什么,但是我们可以知道合约代码中的执行顺序是怎么样的。例如,当遇到 `jump` 指令的时候我们大概就知道这里会有一次跳跃。我们可以通过分析代码的 Opcode 来将合约进一步变成一张 Control Flow Graph (CFG)。\n\n![Control Flow Graph](ipfs://bafkreid4afbilhy467dlrjbr4l4vbklnhnwq75pn3hhqdos3ntmsj46abi)\n\n在 CFG 中,每一个块被称为 Basicblock,比较官方的定义为:\n\n> In compiler construction, a basic block is a straight-line code sequence with no branches in except to the entry and no branches out except at the exit\n\n大概意思就是,一个 Basicblock 表示这一部分代码只能从头部指令进入开始运行,然后从尾部指令结束允许。用人话讲就是一个 Basicblock 里的代码一旦被运行就是一起被运行,不会从中间跳走到另外一个 Basicblock。\n\n有了 CFG 后,我们可以从 `dispatcher()` 中找到合约包含的每一个函数以及每一个函数的偏移,然后通过静态分析可以定位每一个函数包含了哪些 Basicblock。到此,大部分反编译就完成了。\n\n#### 提取指令\n\n由于我们进行的是函数间的相似度计算,我们的处理自然以函数为单位。有了上一小节编译得到的 CFG 后,我们可以按照 CFG 的执行顺序遍历每一个函数所涉及的所有指令。这里,由于我们是按照 CFG 的顺序来遍历的,所以遍历后的结果天然具有一定的语义。最终,我们把每一个指令转化成字符串,不同指令以空格隔开,一个合约的指令就被提取出来了。\n\n#### 向量化\n\n由于一个函数可能包含成千上万的指令,使用基于深度学习的向量化方法可能会过于复杂且低效。这里,我们使用的是基于 `word2vec` 的 `doc2vec` 来无监督地对提取的指令做向量化。如果有条件的话读者完全可以试试基于对比学习的向量化方法,例如基于对比学习的 `codebert`。\n\n在训练的时候,我们使用了 `slither-audited-smart-contracts` 数据集,其包含大约 12 万条合约的 Bytecode。将数据集处理完后会有大约 200 万个函数供我们训练。这么多数据对于 `doc2vec` 来说应该是够用了,但是如果读者想尝试基于对比学习的向量化方法的话,可以试试才开源的 [paradigm-data-portal](https://github.com/paradigmxyz/paradigm-data-portal) 数据集。\n\n#### 合约相似度计算\n\n最后,有了函数间的相似度后,我们定义合约 $C_1$ 和 $C_2$ 的相似度为:\n\n$$Sim(C_1, C_2) = \\sum_{f_i \\in C_1} log \\frac{P(f_i, f_2^*)}{P(f_i, \\bar{f_2})},$$\n\n其中,$f_i$ 表示 $C_1$ 的第 $i$ 个函数, $f_2^*$ 表示 $C_2$ 中与 $f_1$ 最相似的函数, $\\bar{f_2}$ 表示 $f_i$ 与 $C_2$ 中任意函数的平均相似度. $P(f_i, f_2^*)$ 和 $P(f_i, \\bar{f_2})$ 是 $f_i$ 与 $f_2^*$ 和 $\\bar{f_2}$ 分别相似的概率。 $P(\\cdot)$ 表示为:\n\n$$P(f_i, f_j) = \\frac{1}{1 + e^{-k * cos(f_i, f_2)}},$$\n\n其中, $k$ 是一个超参数,$cos(\\cdot, \\cdot)$ 是两个向量的余弦相似度。\n\n最终,有了 $Sim(C_1, C_2)$ 后,我们计算 $C_1$ 和 $C_2$ 的相似度评分为:\n\n$$ score = \\frac{Sim(C_1, C_2)}{Sim(C_1, C_1)}$$\n\n这个 $score$ 可以用来区分相似合约和不相似的合约。",
"attributes": [
{
"value": "tong-guo-Bytecode-ji-suan-he-yue-xiang-si-du",
"trait_type": "xlog_slug"
}
]
}