本文介绍了 OtterSec 发布的用于 Solana 程序逆向工程的 Binary Ninja 插件,该插件支持 Solana-flavored EBPF,并提供了诸如指令提升、精确的内存映射、Solana ELF 重定位、系统调用函数签名以及 Solana SDK 类型等功能,旨在帮助安全研究人员分析闭源的 Solana 程序。
介绍我们用于黑盒 Solana 程序分析的开源 Binary Ninja 插件,以及 Solana 运行时的概要参考。
我们很高兴地宣布发布我们的开源 Solana 的 Binary Ninja 插件!我们一直在努力开发这个工具,以帮助我们进行黑盒 Solana 程序分析。虽然它仍在开发中,但现在已经足够成熟,可以使用(和可用),我们已决定将其发布给更大的安全社区。你可以在 Github 上找到它:https://github.com/otter-sec/bn-ebpf-solana。如果你在我们的插件中发现错误或想添加改进,请提出 issue 或提交 PR!
在这篇博文中,我们将提供一些关于 Solana 运行时的背景信息,并描述我们插件的各种组件。在后续的博文中,我们将深入研究使用此工具反编译和分析闭源 Solana 程序的案例研究。
Solana 程序使用 Rust 或 C(目前)编写,并编译为 Solana 风格的 EBPF ELF 格式。这些 ELF 文件存储在链上的数据账户中,并包含实际运行程序所需的所有信息。通常,编译器会丢弃不相关的、仅供人类使用的信息,例如变量名和函数名。
虽然 EBPF 最初设计用于 Linux 内核,但 Solana 已经修改了一个稍微修改过的版本,用于区块链上。Solana 程序在一个完全隔离的环境中运行,除了两个接口:
input
内存段,用于传递有关程序初始参数的信息(账户、指令信息、交易上下文)。syscalls
,允许 Solana 程序通过明确定义的 API 与运行时交互。内存段被重新定位到 Solana 内存模型中的特定区域:
0x100000000
: Text — 包含来自 ELF 的代码和只读数据0x200000000
: Stack - 用于堆栈数据的专用空间0x300000000
: Heap - 用于堆存储的专用空间0x400000000
: Input — 包含序列化的输入数据。每次程序运行时,运行时都会填充此数据。在 Solana 中,通过发出交易与程序交互。每个交易由一个或多个指令组成。从概念上讲,每个指令负责在特定程序上调用“一个函数”。注意:这不一定是这种情况,但这是大多数 Solana 程序的设计方式。
每个 Instruction
包含以下属性:
progam_id
: 我们要调用的程序的地址。accounts
: 我们将在执行期间使用的一系列账户*data
: 传递给程序的任意数据* Solana 要求我们预先定义在指令期间需要访问哪些账户,以便运行时可以并行化在不相交集合上运行的指令。
与其他一些区块链架构不同,Solana 中的程序只有一个入口点。想要实现多个“函数”的程序需要在用户空间中执行此操作。通常,这由高级框架(如 Anchor)处理。
在二进制级别,通过在 ELF 中定义一个名为“entrypoint”的符号来实现 entrypoint
,该符号指向要运行的第一个函数。例如,在 C 中,这就像定义以下函数一样简单:
extern uint64_t entrypoint(const uint8_t *input) {
// do things
}
为了处理一个指令,运行时执行以下步骤(简而言之):
program_id
账户。accounts
和交易上下文序列化到 VM 的 input
段中。data
作为参数调用程序中的 entrypoint
函数。// ... 程序逻辑在这里运行 ...
input
段,验证更改,并将结果提交回全局状态。Solana 程序也可以直接调用其他程序。这称为跨程序调用 (CPI)。在 Solana 中,这通过系统调用(sol_invoke_signed_c
或 sol_invoke_signed_rust
)实现,它们仅在 ABI 中有所不同。
使用这些系统调用,程序可以发出在其他程序上执行的指令。重要的是,这些嵌套指令中使用的任何账户也必须在初始指令中提供。
在此图中,MyProgram
对某些模拟的 token-mgr
程序执行两次跨程序调用,该程序具有一些 Burn
和 Mint
操作。符号 [Instr()]
表示序列化的枚举。请注意,token-mgr
程序账户必须在原始指令中列出才能对其执行 CPI。另外,请注意对 token-mgr
的两次跨程序调用都调用相同的入口点。要采取的操作由查看提供的 data
中的值确定。
当 Solana 程序调用其中一个系统调用时,当前 VM 暂停,运行时执行与上述相同的步骤,将目标程序加载到新的 VM 中并执行它。跨程序调用完成后,原始 VM 恢复执行。
程序可以通过 sol_set_return_data
和 sol_get_return_data
系统调用设置和获取返回数据,从而允许在这些调用之间传递一些信息。
注意:目前 CPI 有一些限制。具体来说,深度(嵌套调用的数量)限制为 4,并且主要禁止重入
作为审计员,我们始终可以访问客户的源代码。当涉及到理解一段代码如何工作时,这种访问有很多好处:
虽然大多数构建在 Solana 上的协议都将其代码作为开源发布,但有些协议则不这样做。因此,如果没有对源代码的独家审计员访问权限,人们可能会探索其他选项。此外,攻击者肯定不会发布其恶意程序的源代码,那么我们还可以使用哪些其他信息呢?
一种特别有用的技术是查看在 explorer 中在特定程序上执行的先前指令。执行指令时,会存储大量有用的信息:
sol_log_*
系统调用写入日志字符串。通常,程序会记录正在执行的指令的名称,这使你可以轻松地分解指令格式。Instruction
对象,这使我们可以准确地看到调用了哪些程序以及使用了哪些地址和数据。许多 DeFi 协议都会调用Token管理程序(例如 spl-token
),这使我们可以轻松地了解资金的转移方式。虽然这些技术对于分析特定指令的影响很有用(即它调用了什么?,它做了什么?),但到目前为止,我们无法确切地了解程序如何运行:它做出什么决定?它能够做什么?它是如何设计的?
为了深入了解这些更复杂的细节,我们需要实际分析编译后的 EBPF 程序。虽然用于传统原生二进制文件的工具非常先进,但用于 Solana 二进制文件分析的现有工具并不多。因此,我们决定开发我们自己的 Solana 程序分析工具...
bn-ebpf-solana 是 Binary Ninja 的开源插件,支持 Solana 风格的 EBPF。它仍然是一个正在进行中的工作;我们计划添加几个功能,这些功能将使黑盒 Solana 程序分析更加容易。但是,在其当前阶段,它已被证明对我们自己的内部分析很有用,因此我们决定将其开源,以便它也可以对其他人有用!
该插件由两部分组成:
目前,支持以下功能:
LLIL 提升已针对所有 EBPF 指令实现,这允许 Binary Ninja 执行本地静态分析并生成简洁的反编译。
左:CFG 反汇编视图 / 右:线性 HLIL 视图
ELF 段被重定位到 0x100000000
范围,如核心运行时代码所实现的那样。在 0x{2/3/4}00000000
处为 stack
、heap
和 input
段创建了额外的段。
我们应用核心运行时实现的 Solana 特定的 ELF 重定位。
我们识别 Solana 系统调用,将它们转换为调用并应用正确的函数签名。
在此示例中,打开 EBPF .so 文件后,会自动应用 sol_panic_
签名。
我们使用 Solana SDK 类型自动填充分析视图(完全支持 C,Rust 仍在开发中)。这些类型允许完全恢复运行时使用的 Solana 对象的结构。
将此代码与相应的源代码(来自 PicoCTF 2022 的 solfire
挑战)进行比较:
sol_assert(payee->is_signer);
sol_assert(params->data_len - 4 >= sizeof(*args));
args = (withdraw_args*) (params->data + 4);
sol_assert(args->idx <= MARKET_CNT);
sol_assert(args->amt != 0);
uint8_t seed[] = { 'v', 'a', 'u', 'l', 't', 'x' };
seed[5] = args->bump;
const SolSignerSeed seeds[] = {{seed, SOL_ARRAY_SIZE(seed)}};
const SolSignerSeeds signers_seeds[] = {{seeds, SOL_ARRAY_SIZE(seeds)}};
SolAccountMeta arguments[] = {
{vault->key, true, true},
{payee->key, true, false},
};
uint8_t data[4 + sizeof(transfer_amount_sys)];
sol_memset(data, 0, sizeof(data));
*(uint16_t *)data = TRANSFER;
transfer_amount_sys* data_args = (transfer_amount_sys*) (data + 4);
data_args->lamports = args->amt;
const SolInstruction instruction = {system_program->key, arguments,
SOL_ARRAY_SIZE(arguments), data,
SOL_ARRAY_SIZE(data)};
sol_invoke_signed(&instruction, params->ka, params->ka_num,
signers_seeds, SOL_ARRAY_SIZE(signers_seeds));
LendingData* ld_obj = (LendingData*) ld_acct->data;
Market* market = &ld_obj->markets[args->idx];
market->liab += args->amt;
sol_assert(market->liab <= market->collat);
我们计划在这篇介绍性博文之后发布一系列使用我们的 Binary Ninja 插件进行黑盒 Solana 程序分析的案例研究。敬请关注!
- 原文链接: osec.io/blog/2022-08-27-...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!