Skip to content
本页内容由 AI 辅助生成与翻译,如有不当之处,欢迎协助改进。 在 GitHub 上编辑

WASM 插件开发

PRX-SD 包含一个基于 Wasmtime 的插件系统,允许你使用任何可编译为 WebAssembly 的语言(Rust、Go、C、AssemblyScript 等)编写自定义扫描器来扩展检测引擎。插件在沙箱化的 WASM 环境中运行,具有可配置的资源限制。

架构

~/.prx-sd/plugins/
  my-scanner/
    plugin.json          # 插件清单
    my_scanner.wasm      # 编译后的 WASM 模块
  another-plugin/
    plugin.json
    another_plugin.wasm

扫描引擎启动时,PluginRegistry 会遍历插件目录,加载每个包含 plugin.json 的子目录,编译 WASM 模块,并调用插件的 on_load 导出函数。扫描过程中,每个 file_typesplatforms 与当前文件匹配的插件会依次被调用。

执行流程

  1. 发现 -- PluginRegistry~/.prx-sd/plugins/ 中查找 plugin.json 文件
  2. 编译 -- Wasmtime 使用燃料计量和内存限制编译 .wasm 模块
  3. 初始化 -- 调用 on_load();读取 plugin_name()plugin_version()
  4. 扫描 -- 对每个文件,使用文件数据调用 scan(ptr, len) -> score
  5. 报告 -- 插件调用 report_finding() 注册威胁,或返回非零分数

插件清单 (plugin.json)

每个插件目录必须包含一个 plugin.json,用于描述插件及其沙箱约束:

json
{
  "name": "Example Scanner",
  "version": "0.1.0",
  "author": "prx-sd",
  "description": "Example plugin that detects MALICIOUS_MARKER string",
  "wasm_file": "example_plugin.wasm",
  "platforms": ["all"],
  "file_types": ["all"],
  "min_engine_version": "0.1.0",
  "permissions": {
    "network": false,
    "filesystem": false,
    "max_memory_mb": 64,
    "max_exec_ms": 5000
  }
}

清单字段

字段类型必填说明
namestring插件的可读名称
versionstring插件的语义化版本
authorstring插件作者或组织
descriptionstring插件检测内容的简要描述
wasm_filestring编译后的 WASM 模块文件名(相对于插件目录)
platformsstring[]目标平台:"linux""macos""windows""all"
file_typesstring[]要检查的文件类型:"pe""elf""macho""pdf""all"
min_engine_versionstring所需的最低 PRX-SD 引擎版本
permissions.networkboolean插件是否可以访问网络(默认:false
permissions.filesystemboolean插件是否可以通过 WASI 访问宿主文件系统(默认:false
permissions.max_memory_mbinteger最大线性内存(MiB)(默认:64
permissions.max_exec_msinteger最大执行时间(毫秒)(默认:5000

必需的 WASM 导出

你的 WASM 模块必须导出以下函数:

scan(ptr: i32, len: i32) -> i32

主扫描入口点。接收指向客户内存中文件数据的指针和长度。返回 0 到 100 的威胁分数:

  • 0 = 安全
  • 1-29 = 信息性
  • 30-59 = 可疑
  • 60-100 = 恶意

memory

模块必须将其线性内存导出为 memory,以便宿主可以写入文件数据和读取结果。

可选的 WASM 导出

导出签名说明
on_load() -> i32() -> i32编译后调用一次。返回 0 表示成功。
plugin_name(buf: i32, len: i32) -> i32(i32, i32) -> i32将插件名称写入缓冲区。返回实际长度。
plugin_version(buf: i32, len: i32) -> i32(i32, i32) -> i32将插件版本写入缓冲区。返回实际长度。
alloc(size: i32) -> i32(i32) -> i32分配 size 字节的客户内存。返回指针。

插件可用的宿主函数

宿主在 "env" 命名空间中提供以下函数:

report_finding(name_ptr, name_len, score, detail_ptr, detail_len)

报告威胁发现。在单次扫描中可多次调用。

  • name_ptr / name_len -- 威胁名称字符串的指针和长度(例如 "Trojan.Marker"
  • score -- 威胁分数(0-100,会被截断)
  • detail_ptr / detail_len -- 详情字符串的指针和长度

log_message(level, msg_ptr, msg_len)

将日志消息写入引擎的追踪系统。

  • level -- 0=trace、1=debug、2=info、3=warn、4=error
  • msg_ptr / msg_len -- 消息字符串的指针和长度

get_file_path(buf_ptr, buf_len) -> actual_len

将正在扫描的文件路径读入客户缓冲区。

get_file_type(buf_ptr, buf_len) -> actual_len

将检测到的文件类型(例如 "pe""elf""pdf")读入客户缓冲区。

PluginFinding 结构体

当插件报告发现(通过 report_finding() 或返回非零分数)时,引擎会创建一个 PluginFinding

rust
pub struct PluginFinding {
    pub plugin_name: String,   // 插件名称
    pub threat_name: String,   // 例如 "Trojan.Marker"
    pub score: u32,            // 0-100
    pub detail: String,        // 自由格式的详情字符串
}

如果插件返回非零分数但没有调用 report_finding(),引擎会自动合成一个发现:

threat_name: "Plugin.<plugin_name>"
detail: "Plugin '<name>' returned threat score <score>"

开发流程

1. 创建插件目录

bash
mkdir -p ~/.prx-sd/plugins/my-scanner

2. 编写清单

bash
cat > ~/.prx-sd/plugins/my-scanner/plugin.json << 'EOF'
{
  "name": "My Custom Scanner",
  "version": "0.1.0",
  "author": "your-name",
  "description": "Detects custom threat patterns",
  "wasm_file": "my_scanner.wasm",
  "platforms": ["all"],
  "file_types": ["all"],
  "min_engine_version": "0.1.0",
  "permissions": {
    "network": false,
    "filesystem": false,
    "max_memory_mb": 64,
    "max_exec_ms": 5000
  }
}
EOF

3. 编写插件(Rust 示例)

创建新的 Rust 库项目:

bash
cargo new --lib my-scanner
cd my-scanner

Cargo.toml 中添加:

toml
[lib]
crate-type = ["cdylib"]

[profile.release]
opt-level = "s"
lto = true

编写 src/lib.rs

rust
// 宿主函数导入
extern "C" {
    fn report_finding(
        name_ptr: *const u8, name_len: u32,
        score: u32,
        detail_ptr: *const u8, detail_len: u32,
    );
    fn log_message(level: u32, msg_ptr: *const u8, msg_len: u32);
}

#[no_mangle]
pub extern "C" fn on_load() -> i32 {
    let msg = b"My Custom Scanner loaded";
    unsafe { log_message(2, msg.as_ptr(), msg.len() as u32) };
    0 // success
}

#[no_mangle]
pub extern "C" fn scan(ptr: *const u8, len: u32) -> i32 {
    let data = unsafe { core::slice::from_raw_parts(ptr, len as usize) };

    // 示例:查找已知的恶意标记
    let marker = b"MALICIOUS_MARKER";
    if data.windows(marker.len()).any(|w| w == marker) {
        let name = b"Custom.MaliciousMarker";
        let detail = b"Found MALICIOUS_MARKER string in file data";
        unsafe {
            report_finding(
                name.as_ptr(), name.len() as u32,
                85,
                detail.as_ptr(), detail.len() as u32,
            );
        }
        return 85;
    }

    0 // clean
}

4. 编译为 WASM

bash
rustup target add wasm32-wasip1
cargo build --release --target wasm32-wasip1
cp target/wasm32-wasip1/release/my_scanner.wasm ~/.prx-sd/plugins/my-scanner/

5. 测试插件

bash
# 创建包含标记的测试文件
echo "MALICIOUS_MARKER" > /tmp/test-marker.txt

# 使用调试日志扫描以查看插件活动
sd --log-level debug scan /tmp/test-marker.txt

TIP

使用 --log-level debug 可查看详细的插件加载和执行信息,包括燃料消耗和内存使用情况。

沙箱安全性

插件在 Wasmtime 沙箱中运行,受以下约束:

约束执行方式
内存限制清单中的 max_memory_mb;Wasmtime 强制执行线性内存上限
CPU 限制max_exec_ms 转换为燃料单位;燃料耗尽时执行被终止
网络默认禁用;需要 permissions.network: true
文件系统默认禁用;需要 permissions.filesystem: true(WASI 预开放)
平台检查platforms 不匹配的插件在加载时跳过
文件类型过滤file_types 不匹配的插件在逐文件处理时跳过

WARNING

即使设置了 network: truefilesystem: true,WASI 沙箱也会将访问限制在特定目录和端点。这些权限是意图声明,而非无限制的访问授权。

热重载

将新的插件目录放入 ~/.prx-sd/plugins/,注册表会在下次扫描时自动加载。对于守护进程,可通过调用 sd update 或重启守护进程来触发重载。

后续步骤

Released under the Apache-2.0 License.