您正在查看: 2025年4月

私有链化部署Squads Protocol

前期调研

  • 目前v3已经停止升级,优先使用v4版本
  • Squads-Protocol 官方app.squads.so前端不开源,只开源了精简版本 squads-backup-kit
  • squads-backup-kit 前端不支持创建,只支持管理已有多签账户地址
  • 首次创建可以使用Squads CLI进行创建
  • v4程序地址:SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf

squads-backup-kit

GitHub:https://github.com/Squads-Protocol/public-v4-client
在线地址:https://backup.app.squads.so/

Squads CLI 使用

https://docs.squads.so/main/development/cli/installation
https://docs.squads.so/main/development/cli/commands

创建keypair

mkdir squads-multisig
cd squads-multisig
solana-keygen new -o ./keypair-multisig.json
solana-keygen new -o ./keypair-1.json
solana-keygen new -o ./keypair-2.json
solana-keygen new -o ./keypair-3.json

生成测试账户地址

89CefQR7XpdSKsDAFMZVUu3ipqiW35BceuahoE8CiAxz
56uqkuNMGsozcYDuzKfkqUEPqDLQfXRncvoH73xHzHnt
G3YLqbreJj1spvBMajv8qgPXVzZ1WaN34LDn7ChFdAPY
Hj3KcgcuvcHRM9or6SbuLuTV8Xe5pDttKk3nQSXzP65d

分别给上面4个账户地址转1SOL,为后面演示 2/3多签测试做准备

创建多重签名

squads-multisig-cli multisig-create --rpc-url <RPC_URL> --program-id <PROGRAM_ID> --keypair <KEYPAIR_PATH> --config-authority <CONFIG_AUTHORITY> --members <MEMBER_1> <MEMBER_2> ... --threshold <THRESHOLD>

multisig-create参数

  • --rpc-url :(可选)Solana RPC 端点的 URL。如果未指定,则默认为主网。
  • --program-id :(可选)多重签名程序的 ID。如未指定,则默认为标准 ID。
  • --keypair :密钥对文件的路径。
  • --config-authority :(可选)程序配置机构的地址。
  • --members <MEMBER_...>:成员公钥列表,以空格分隔。
  • --threshold :执行多重签名交易所需的签名阈值数量。

测试创建

squads-multisig-cli multisig-create --rpc-url https://api.devnet.solana.com --program-id SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf --keypair ./keypair-multisig.json --members 56uqkuNMGsozcYDuzKfkqUEPqDLQfXRncvoH73xHzHnt,7 --members G3YLqbreJj1spvBMajv8qgPXVzZ1WaN34LDn7ChFdAPY,7 --members Hj3KcgcuvcHRM9or6SbuLuTV8Xe5pDttKk3nQSXzP65d,7 --threshold 2

threshold为2,则代表 2/3通过

权限是数字,映射到以下内容:

  • 提议人 -1
  • 投票人 -2
  • 执行人 -4
  • 上述所有的 -7

或者任何权限组合(即提议者和投票者3)
例如 --members FcBpwMquaMURbYwpRFUrBrYgFwJzfWiBEGfHLbik1Wsm,7

执行命令后返回
https://solscan.io/tx/XRFQkd2HN4mFoQvKa2FUdKYr2WkETZmxAYbCkM87VjxFDBgUWCEQYEixPcodpQtu1kv8cVgu1MP5pQpcU995U9A?cluster=devnet

You're about to create a multisig, please review the details:

RPC Cluster URL:   https://api.devnet.solana.com
Program ID:        SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf
Your Public Key:       89CefQR7XpdSKsDAFMZVUu3ipqiW35BceuahoE8CiAxz

Config Parameters

Config Authority:  None
Threshold:          2
Rent Collector:     None
Members amount:      3

Do you want to proceed? yes

⠒ Sending transaction...                                                                                                Transaction confirmed: XRFQkd2HN4mFoQvKa2FUdKYr2WkETZmxAYbCkM87VjxFDBgUWCEQYEixPcodpQtu1kv8cVgu1MP5pQpcU995U9A


Created Multisig: 9VrXzJ8zQ8PZQ1pBnzbfNVB8cJKneU6rdrjwUKypAybE. Signature: XRFQkd2HN4mFoQvKa2FUdKYr2WkETZmxAYbCkM87VjxFDBgUWCEQYEixPcodpQtu1kv8cVgu1MP5pQpcU995U9A

多签钱包地址为:9VrXzJ8zQ8PZQ1pBnzbfNVB8cJKneU6rdrjwUKypAybE

使用前端工具进行测试

https://backup.app.squads.so/

查看当前多签配置

https://backup.app.squads.so/#/config/

修改配置交易创建

squads-multisig-cli config-transaction-create --rpc-url <RPC_URL> --program-id <PROGRAM_ID> --keypair <KEYPAIR_PATH> --multisig-pubkey <MULTISIG_PUBLIC_KEY> --action <ACTION> [--memo <MEMO>]

添加新成员

squads-multisig-cli config-transaction-create --rpc-url https://api.devnet.solana.com --program-id SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf --keypair ./keypair-multisig.json --multisig-pubkey 89CefQR7XpdSKsDAFMZVUu3ipqiW35BceuahoE8CiAxz --action "AddMember 56uqkuNMGsozcYDuzKfkqUEPqDLQfXRncvoH73xHzHnt 7" --memo "AddMember 56uqkuNMGsozcYDuzKfkqUEPqDLQfXRncvoH73xHzHnt 7"

其余修改操作参考 https://docs.squads.so/main/development/cli/commands

私有链部署

对于私有链部署,首先需要初始化程序so

solana program -u m dump SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf ./roles/svm-node/files/spl/squads-protocol-v4.so
solana-genesis --upgradeable-program SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf BPFLoaderUpgradeab1e11111111111111111111111 {{path}}/config/spl/squads-protocol-v4.so {{upgradeAccount}} 

链启动后,创建多签交易报错

⠂ Sending transaction...                                                                                                thread 'main' panicked at /root/.cargo/registry/src/index.crates.io-6f17d22bba15001f/squads-multisig-cli-0.1.3/src/command/multisig_create.rs:141:14:
Failed to fetch program config account: Error { request: None, kind: RpcError(ForUser("AccountNotFound: pubkey=BSTq9w3kZwNwpBXJEvTZz2G9ZTNyKBvoSeXMvwb4cNZr")) }

分析:缺少内部初始化账户

获取账户地址Data {注意需要base64格式}

curl https://api.devnet.solana.com -s -X \
  POST -H "Content-Type: application/json" -d ' 
  {
    "jsonrpc": "2.0",
    "id": 1,
    "method": "getAccountInfo",
    "params": [
      "BSTq9w3kZwNwpBXJEvTZz2G9ZTNyKBvoSeXMvwb4cNZr",
      {
        "encoding": "base64"
      }
    ]
  }
' | jq

返回

{
  "jsonrpc": "2.0",
  "result": {
    "context": {
      "apiVersion": "2.2.3",
      "slot": 373312028
    },
    "value": {
      "data": [
        "xNJa55CVjD/y4DkJcttfEtTBz1IxzaY5194JtJU/4aL4ooeXbGx+0AAAAAAAAAAA8uA5CXLbXxLUwc9SMc2mOdfeCbSVP+Gi+KKHl2xsftAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA",
        "base64"
      ],
      "executable": false,
      "lamports": 1893120,
      "owner": "SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf",
      "rentEpoch": 18446744073709551615,
      "space": 144
    }
  },
  "id": 1
}

调整初始化参数,加载预设账户地址和data

solana-genesis --primordial-accounts-file accounts.json

accounts.json

{
  "BSTq9w3kZwNwpBXJEvTZz2G9ZTNyKBvoSeXMvwb4cNZr": {
    "balance": 100000000000,
    "owner": "SQDS4ep65T869zMMBKyuUq6aD6EgTu8psMjkvj52pCf",
    "executable": false,
    "data": "xNJa55CVjD/y4DkJcttfEtTBz1IxzaY5194JtJU/4aL4ooeXbGx+0AAAAAAAAAAA8uA5CXLbXxLUwc9SMc2mOdfeCbSVP+Gi+KKHl2xsftAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA"
  }
}

再次确认私链账户地址与solana devnet data数据一致

solana account BSTq9w3kZwNwpBXJEvTZz2G9ZTNyKBvoSeXMvwb4cNZr --url https://api.devnet.solana.com

参考

https://docs.eclipse.xyz/developers/developer-tooling/squads-multisig
https://docs.squads.so/main/additional-resources/what-if-the-squads-app-goes-down
https://github.com/assetCLI/assetCLI-init/blob/feb-25/local-dev.sh

Solana IDL 猜测器 - 从闭源 Solana 程序中恢复指令布局

Solana 生态系统充满活力,但许多程序尚未开源。Syndica在 2024 年 2 月编制的统计数据显示,按计算单元计算,Solana 排名前 100 的程序中,近 50% 发布了其接口定义语言 (IDL)。然而,对于排名前 1000 的程序,这一数字下降到只有 20%。此外,即使已发布的 IDL 也并不总是可靠的。发布的 IDL 过时且与部署的链上程序不匹配的情况并不少见。

作为审计人员和安全研究人员,当我们发现可能导致漏洞的有趣模式时,我们通常会在其他程序中寻找类似的弱点。然而,如果没有源代码或准确的 IDL,这个过程通常仅限于基本的 GitHub 搜索,经常会发现无人维护的项目。

大多数 Solana 程序都是用 Rust 编写的,并编译为 Solana 字节码格式 (sBPF),这是一种基于 eBPF 的格式。对编译后的 Rust 进行逆向工程具有挑战性,而与 sBPF 相关的逆向工程工具链仍在开发中。这种不透明性不仅阻碍了恶意行为者,而且还减缓了白帽黑客和安全研究人员识别和负责任地披露漏洞的工作。

要分析任何闭源 Solana 程序(无论是动态还是静态),基本前提是了解如何与其交互。这意味着了解它的指令、每条指令所需的账户以及这些账户的属性(如签名者或可写状态)。

为了应对这些挑战,我们的安全研究员齐秦领导了这项工作并开发了一个名为IDL Guesser的原型工具。该工具旨在直接从闭源 Solana 程序二进制文件中自动恢复指令定义、所需帐户(包括签名者/可写标志)和参数信息。

该博客概述了 IDL Guesser 背后的方法并讨论了未来改进的潜在领域。

利用锚点模式进行逆向工程

由于 Solana 开发的大部分内容都使用了 Anchor 框架(IDL 的概念也源于此),因此 IDL Guesser 目前专注于基于 Anchor 的程序。Anchor 通过为常见任务和检查提供宏和辅助函数,大大简化了开发。至关重要的是,这会导致编译输出中出现可预测的标准化代码结构,我们可以通过模式匹配将其用于分析。

为了进行调试,Anchor CLI 甚至提供了一个anchor expand\命令,该命令会显示由其宏生成的代码。检查此扩展代码可以深入了解 Anchor 所采用的模式,从而指导我们的逆向工程工作。

入口点和调度逻辑

我们来看看一个典型的 Anchor 程序经过宏扩展后的入口点结构:

#[no_mangle]
pub unsafe extern "C" fn entrypoint(input: *mut u8) -> u64 {
    let (program_id, accounts, instruction_data) = unsafe {
        ::solana_program::entrypoint::deserialize(input)
    };
    match entry(&program_id, &accounts, &instruction_data) {
        Ok(()) => ::solana_program::entrypoint::SUCCESS,
        Err(error) => error.into(),
    }
}

pub fn entry<'info>(
    program_id: &Pubkey,
    accounts: &'info [AccountInfo<'info>],
    data: &[u8],
) -> anchor_lang::solana_program::entrypoint::ProgramResult {
    try_entry(program_id, accounts, data)
        .map_err(|e| {
            e.log();
            e.into()
        })
}

fn try_entry<'info>(
    program_id: &Pubkey,
    accounts: &'info [AccountInfo<'info>],
    data: &[u8],
) -> anchor_lang::Result<()> {
    if *program_id != ID {
        return Err(anchor_lang::error::ErrorCode::DeclaredProgramIdMismatch.into());
    }
    if data.len() < 8 {
        return Err(anchor_lang::error::ErrorCode::InstructionMissing.into());
    }
    dispatch(program_id, accounts, data)
}

fn dispatch<'info>(
    program_id: &Pubkey,
    accounts: &'info [AccountInfo<'info>],
    data: &[u8],
) -> anchor_lang::Result<()> {
    let mut ix_data: &[u8] = data;
    let sighash: [u8; 8] = {
        let mut sighash: [u8; 8] = [0; 8];
        sighash.copy_from_slice(&ix_data[..8]);
        ix_data = &ix_data[8..];
        sighash
    };
    use anchor_lang::Discriminator;
    match sighash {
        instruction::InitializeConfig::DISCRIMINATOR => {
            __private::__global::initialize_config(program_id, accounts, ix_data)
        }
        instruction::InitializePool::DISCRIMINATOR => {
            __private::__global::initialize_pool(program_id, accounts, ix_data)
        }
        // ... other instructions
    }
}

程序首先对原始输入进行反序列化。它在进入函数之前执行基本检查(例如验证program_id\并确保instruction_data\至少有 8 个字节长)dispatch\。在 中dispatch\, 的前 8 个字节instruction_data\被解释为指令鉴别符。该鉴别符确定应执行哪个特定的指令处理程序函数。

根据 Anchor 的文档,这个 8 字节鉴别器是从指令的命名空间和名称(例如global:initialize_config\)中派生出来的,方法是取其 SHA-256 哈希的前 8 个字节。IDL Guesser 不会尝试从编译后的代码中提取这些原始鉴别器字节(这可能很复杂),而是采用一种更简单的方法:它首先专注于提取指令名称,然后使用 Anchor 的标准哈希方法计算相应的鉴别器。

识别指令处理程序

我们如何找到指令名称及其对应的处理程序函数?Anchor 提供了另一种有用的模式。考虑一个典型指令处理程序的开头:

pub fn initialize_config<'info>( /* ... */ ) -> anchor_lang::Result<()> {
    // Log the instruction name - a key pattern for us!
    ::solana_program::log::sol_log("Instruction: InitializeConfig");

    // Deserialize instruction-specific parameters
    let ix = instruction::InitializeConfig::deserialize(&mut &__ix_data[..])
        .map_err(|_| /* ... */ )?;
    let instruction::InitializeConfig { /* ... parameters ... */ } = ix;

    // Process accounts via try_accounts
    let mut __bumps = /* ... */;
    let mut __reallocs = /* ... */;
    let mut __remaining_accounts: &[AccountInfo] = __accounts;
    let mut __accounts = InitializeConfig::try_accounts(
        __program_id,
        &mut __remaining_accounts,
        __ix_data,
        &mut __bumps,
        &mut __reallocs,
    )?;
    // ... 
}

Anchorsol_log\在每个处理程序的开头插入一个调用,记录指令的名称(例如“Instruction: InitializeConfig”\)以供日志解析。此日志记录提供了我们可以在编译后的二进制文件中搜索的独特签名。

img

用于记录指令名称*InitializeConfig 的*****sol_log\系统调用序列。

在汇编级别(如上所示),此日志调用通常会转换为特定的lddw\mov64\call\指令设置,sol_log\以使用指令名称字符串调用系统调用。

通过识别这些模式,IDL Guesser 可以可靠地定位指令处理程序的入口点并提取其名称。

提取账户信息

在初始日志记录和参数反序列化(通常由编译器内联)之后,处理程序通常会调用相应的try_accounts\函数。此函数负责解析和验证指令所需的帐户。

img

参数反序列化之后调用try_accounts\函数(此处为sub_662B0\)。

让我们检查一下Accounts\结构和生成的try_accounts\函数initialize_config\

// Account definition
#[derive(Accounts)]
pub struct InitializeConfig<'info> {
    #[account(init, payer = funder, space = WhirlpoolsConfig::LEN)]
    pub config: Account<'info, WhirlpoolsConfig>, // To be initialized
    #[account(mut)]
    pub funder: Signer<'info>,                   // Must be mutable and a signer
    pub system_program: Program<'info, System>,  // System program
}

// Generated try_accounts function (simplified)
fn try_accounts( /* ... */ ) -> anchor_lang::Result<Self> {
    // Check if enough accounts provided
    if __accounts.is_empty() {
        // Error 3005
        return Err(anchor_lang::error::ErrorCode::AccountNotEnoughKeys.into()); 
    }
    // Process 'config' account (index 0) - checks applied later
    let config = &__accounts[0];
    *__accounts = &__accounts[1..];

    // Process 'funder' account using Signer's try_accounts
    let funder: Signer = anchor_lang::Accounts::try_accounts(/* ... */)
        .map_err(|e| e.with_account_name("funder"))?; // Adds "funder" to error message

    // Process 'system_program' account
    let system_program: Program<System> = anchor_lang::Accounts::try_accounts(/* ... */)
        .map_err(|e| e.with_account_name("system_program"))?; // Adds "system_program"

    // Apply constraints checks
    if !config.is_writable { // Check defined indirectly via init
            // Error 2000
        return Err(anchor_lang::error::ErrorCode::ConstraintMut.into().with_account_name("config")); 
    }
    if !config.is_signer { // Check defined indirectly via init
       // Error 2002
       return Err(anchor_lang::error::ErrorCode::ConstraintSigner.into().with_account_name("config")); 
    }
    // ... other checks like rent exemption (Error 2005), owner, etc.

    if !funder.is_writable { // Check defined by #[account(mut)]
        // Error 2000
        return Err(anchor_lang::error::ErrorCode::ConstraintMut.into().with_account_name("funder")); 
    }
    // ... checks for funder.is_signer happen inside its own try_accounts

    Ok(Self { config, funder, system_program })
}

try_accounts\函数执行几个关键操作:

  1. 它会遍历预期的帐户,并尝试根据其类型(Account\Signer\、等)对其进行解析。如果在嵌套调用(如 for )Program\中解析失败,Anchor 会方便地将帐户名称(例如“funder”)附加到错误消息中。这使我们能够通过查找这些特定的错误处理模式来提取帐户名称。*try_accounts*****funder\
  2. 它根据结构中定义的属性应用约束检查(mut\signer\has_one\seeds\、 、租金豁免等) 。重要的是,每个约束违规通常映射到一个唯一的Anchor (例如,is 2000\、is 2002\)。*owner**Accounts**ErrorCode**ConstraintMut**ConstraintSigner*

img

检查需要初始化的帐户是否有足够的帐户密钥,如果失败则分支到错误3005\

img

约束检查条件跳转,导致特定的错误代码如2000\ConstraintMut\)或2002\ConstraintSigner\)。

通过分析函数的控制流图(CFG)try_accounts\,具体来说是遵循“快乐路径”(成功执行),IDL Guesser 可以拼凑出所需的账户:

  • 账户处理的顺序揭示了账户的预期顺序。
  • 与错误消息相关的字符串文字(例如“funder”)显示帐户名称。
  • 失败路径上出现的特定错误代码(如 2000、2002、2005)表示对每个帐户应用的约束(可变、签名者、可变等)。

提取参数

虽然提取指令名称和账户详细信息依赖于相对不同的模式(日志字符串、错误代码、特定函数调用),但恢复有关指令参数的信息更加困难。

Anchor 通常不会生成针对单个参数反序列化失败的详细错误消息。这意味着 Rust 源代码中定义的原始参数名称通常会在编译期间丢失。此外,负责从切片中顺序反序列化参数的代码ix_data\通常会由编译器优化和内联,这使得可靠的汇编级模式匹配非常困难。

ix_data\更有希望的未来方向可能涉及符号执行,通过分析如何使用来确定预期的字节长度以及可能确定每个参数的类型。

然而,在 IDL Guesser 原型中,采用了一种利用动态分析的替代且更简单的方法:由于交易大小限制,Solana 指令数据通常很短,因此我们可以迭代探测处理程序函数。通过稍微增加模拟输入数据的长度并观察执行跟踪中的变化(例如,通过之前失败的检查),我们可能会在新的反序列化步骤成功时推断出边界,并可能推断出参数的类型。

这种迭代反馈循环技术也用于重建账户的内部布局。具体细节这里就不展开了,感兴趣的读者可以去源代码中探索相关的实现。此外,它还可能用于验证或完善恢复的指令和账户信息。

实现细节

识别出这些模式后,实施过程涉及反汇编 sBPF 字节码并对指令序列和 CFG 执行模式匹配。我们以现有solana-sbpf\项目(参见static_analysis.rs)为基础,为 Solana 程序分析奠定了基础。

我们对基本静态分析实现进行了一些修改。原始版本倾向于在每次函数调用后过度拆分基本块。我们对此进行了调整,以创建更大、更易于管理的块。此外,我们还为系统调用(如abort\panic\)添加了特殊处理。这些更改使 CFG 更加精确,从而简化了模式匹配过程。

完整的实现是开源的,可以在IDLGuesser 存储库中获取。

当前代码处理许多常见场景,但也包括一些特殊情况的逻辑,例如UncheckedAccount\Sysvar\帐户。

这些逻辑try_accounts\通常由编译器内联,从而创建类似于init帐户的模式。然而,挑战仍然存在,特别是对于多个连续UncheckedAccount实例或更复杂的结构(如可选帐户和嵌套帐户上下文),此原型尚未完全处理。

结论

IDL Guesser 展示了一种可行的方法,利用框架的代码生成模式和简单的动态分析,从基于 Anchor 的闭源 Solana 程序中恢复基本结构信息(具有相应帐户和参数信息的指令)。虽然原型有局限性,在复杂情况下可能需要手动进行逆向工程并与链上数据进行交叉引用,但它成功地为大量程序恢复了类似 IDL 的信息。

我们发现此功能很有用,可以更广泛地分析交易数据,并有助于自动扫描与帐户限制相关的基本漏洞(例如缺少签名者检查)。通过揭示闭源程序的内部工作原理,我们希望 IDL Guesser 等工具能够为保护 Solana 生态系统做出贡献。

原文:https://www.sec3.dev/blog/idl-guesser-recovering-instruction-layouts-from-closed-source-solana-programs

搜索