本文揭示了CosmWasm运行时的一个栈溢出漏洞,该漏洞源于write_to_contract
函数在分配内存时会回调WASM合约的allocate
函数,恶意合约可利用此机制进行递归调用,最终导致栈溢出。文章提供了PoC、修复建议以及事件时间线。
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) 中添加一项检查,以强制执行运行时和合约之间的最大调用深度。
- 原文链接: github.com/JumpCrypto/se...
- 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!