CosmWasm allocate 栈溢出

本文揭示了CosmWasm运行时的一个栈溢出漏洞,该漏洞源于write_to_contract函数在分配内存时会回调WASM合约的allocate函数,恶意合约可利用此机制进行递归调用,最终导致栈溢出。文章提供了PoC、修复建议以及事件时间线。

CosmWasm 分配栈溢出

摘要

CosmWasm 运行时定义了几个导入(imports),这些函数可以从 WASM 合约中调用,以写入状态更改、执行验证或将昂贵的加密操作卸载到原生实现。

这些函数使用一个名为 write_to_contract 的辅助方法,将结果或错误消息写入 WASM 地址空间:

fn write_to_contract<A: BackendApi, S: Storage, Q: Querier>(
    env: &Environment<A, S, Q>,
    input: &[u8],
) -> VmResult<u32> {
    let out_size = to_u32(input.len())?;
    let result = env.call_function1("allocate", &[out_size.into()])?; **(A)**
    let target_ptr = ref_to_u32(&result)?;
    if target_ptr == 0 {
        return Err(CommunicationError::zero_address().into());
    }
    write_region(&env.memory(), target_ptr, input)?;
    Ok(target_ptr)
}

有趣的是,write_to_contract 本身在 (A) 中回调到 WASM 运行时。调用 allocate 函数用于要求智能合约在其地址空间中分配一个足够大的内存块,以便运行时可以写入。

在一个非恶意的合约中,allocate 函数由 cosmwasm-std 标准库提供,并且这种模式可以正常工作。

但是,一个恶意的合约可以很容易地提供它自己的实现,通过深度嵌套的递归触发栈溢出:allocate 可以不直接返回一个空闲内存范围的地址,而是回调到 Cosmwasm 运行时。如果这个 import 再次触发 write_to_contract,我们最终会进入一个递归循环。

一个简单的例子是 addr_validate import:

pub fn do_addr_validate<A: BackendApi, S: Storage, Q: Querier>(
    env: &Environment<A, S, Q>,
    source_ptr: u32,
) -> VmResult<u32> {
    let source_data = read_region(&env.memory(), source_ptr, MAX_LENGTH_HUMAN_ADDRESS)?;
    if source_data.is_empty() {
        return write_to_contract::<A, S, Q>(env, b"Input is empty");
    }
[..]

当使用空输入调用时,该函数会立即使用错误消息调用 write_to_contract。(这发生在 Gas 费用扣除之前,因此 FFI 调用不会产生任何额外成本)

通过将对 addr_validate 的调用添加到我们合约的 allocate 函数中,我们在合约实例化期间得到了以下调用图,该调用图重复执行直到进程因栈溢出而崩溃:

runtime:instantiate → wasm:allocate → runtime:do_addr_validate → wasm:allocate → runtime:do_addr_validate → wasm:allocate → runtime:do_addr_validate → ..

概念验证

将附加的补丁应用于上游 cosmwasm repo,构建任何包含的合约并运行它们的集成测试。你应该会看到一条错误消息,表明栈溢出:fatal runtime error: stack overflow

在链上用恶意的 allocate() 实现实例化一个合约,会导致所有验证器崩溃并停止链。

diff --git a/packages/std/src/memory.rs b/packages/std/src/memory.rs
index c331a53c..09462d25 100644
--- a/packages/std/src/memory.rs
+++ b/packages/std/src/memory.rs
@@ -15,9 +15,21 @@ pub struct Region {
     pub length: u32,
 }

+
+extern "C" {
+    fn addr_validate(source_ptr: u32) -> u32;
+}
+
 /// Creates a memory region of capacity `size` and length 0. Returns a pointer to the Region.
 /// This is the same as the `allocate` export, but designed to be called internally.
 pub fn alloc(size: usize) -> *mut Region {
+
+    let source = build_region("".as_bytes());
+    let source_ptr = &*source as *const Region as u32;
+
+    let result = unsafe { addr_validate(source_ptr) };
+
+
     let data: Vec<u8> = Vec::with_capacity(size);
     let data_ptr = data.as_ptr() as usize;

建议修复

call_function (https://github.com/CosmWasm/cosmwasm/blob/32f308a1a56ae5b8278947891306f7a374c3df94/packages/vm/src/environment.rs#L164) 中添加一项检查,以强制执行运行时和合约之间的最大调用深度。

时间线

  • 2023-02-27 报告给 CosmWasm 安全团队
  • 2023-04-18 补丁 发布
  • 2023-04-20 CosmWasm 咨询 发布
  • 2023-06-01 Jump Crypto 安全建议发布
  • 原文链接: github.com/JumpCrypto/se...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
JumpCrypto
JumpCrypto
江湖只有他的大名,没有他的介绍。