网络攻防基础实验三漏洞分析

哈哈,免费劳动力来喽

声明,仅供参考,可能存在编造/ai成分,看看就行

题目要求

根据课程所学对20个“疑似”漏洞进行分析:
分析对象文件包含信息:编号(id)、github路径、函数名、函数体、可能属于的CWE-ID
对于确定是漏洞的函数,给出PoC;对于确定不是漏洞的,给出详细解释

对分析对象文件中每个函数提交报告一份:

  • 对于确定是漏洞的函数,要求:
    实验报告中含有漏洞原理(重点考察部分)、攻击步骤(带截图)、消除此漏洞的方法同时附带一份PoC的源代码
  • 对于确定不是漏洞的,要求:
    实验报告中含有分析过程(重点考察部分)、确定不是漏洞的原因、难点与总结
  • 可以调用、复用现有的开源项目库文件或代码,但应在报告中注明来源。

项目:qt-avif-image-plugin

GitHub - novomesk/qt-avif-image-plugin: Qt plug-in to allow Qt and KDE based applications to read/write AVIF images.

环境

依赖

1
2
sudo apt install qtbase5-dev qtbase5-dev-tools qt5-qmake libavif-dev libaom-dev
./build_libqavif_dynamic.sh

把libqavif.so 复制到安装 qt5-image-formats-plugins的imageformats文件夹里

1
sudo cp plugins/imageformats/libqavif.so /usr/lib/x86_64-linux-gnu/qt5/plugins/imageformats/

测试

1
2
3
g++ -fPIC test_avif.cpp -o test_avif \
`pkg-config --cflags --libs Qt5Core Qt5Gui`
QT_DEBUG_PLUGINS=1 ./test_avif cat.avif

正常

image-20260125173820733

101:aom_lpf_horizontal_8_dual_neon

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/aom_dsp/arm/loopfilter_neon.c
函数:aom_lpf_horizontal_8_dual_neon
CWE-415 :double free

分析

1
2
3
4
5
6
7
void aom_lpf_horizontal_8_dual_neon(
uint8_t *s, int pitch, const uint8_t *blimit0, const uint8_t *limit0,
const uint8_t *thresh0, const uint8_t *blimit1, const uint8_t *limit1,
const uint8_t *thresh1) {
aom_lpf_horizontal_8_neon(s, pitch, blimit0, limit0, thresh0);
aom_lpf_horizontal_8_neon(s + 4, pitch, blimit1, limit1, thresh1);
}

不是CWE-415漏洞,文件完全没有使用动态内存分配,没有free问题

102:av1_get_frame_buffer

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/av1/common/frame_buffers.c
函数:av1_get_frame_buffer
CWE:CWE-Other

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
int av1_get_frame_buffer(void *cb_priv, size_t min_size,
aom_codec_frame_buffer_t *fb) {
int i;
InternalFrameBufferList *const int_fb_list =
(InternalFrameBufferList *)cb_priv;
if (int_fb_list == NULL) return -1;

for (i = 0; i < int_fb_list->num_internal_frame_buffers; ++i) {
if (!int_fb_list->int_fb[i].in_use) break;
}

if (i == int_fb_list->num_internal_frame_buffers) return -1;

if (int_fb_list->int_fb[i].size < min_size) {
aom_free(int_fb_list->int_fb[i].data);
int_fb_list->int_fb[i].data = (uint8_t *)aom_calloc(1, min_size);
if (!int_fb_list->int_fb[i].data) {
int_fb_list->int_fb[i].size = 0;
return -1;
}
int_fb_list->int_fb[i].size = min_size;
}

fb->data = int_fb_list->int_fb[i].data;
fb->size = int_fb_list->int_fb[i].size;
int_fb_list->int_fb[i].in_use = 1;

fb->priv = &int_fb_list->int_fb[i];
return 0;
}

漏洞原理

aom_free之后alloc有检查int_fb_list->int_fb[i].data是否分配成功,不成的情况下有size置零和返回,但是data这个指针没free,成为悬垂指针了,可能造成UAF或者double free

调用:qt-avif-image-plugin/ext/libavif/ext/aom/av1/av1_dx_iface.c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (ctx->get_ext_fb_cb != NULL && ctx->release_ext_fb_cb != NULL) {
pool->get_fb_cb = ctx->get_ext_fb_cb;
pool->release_fb_cb = ctx->release_ext_fb_cb;
pool->cb_priv = ctx->ext_priv;
} else {
pool->get_fb_cb = av1_get_frame_buffer;
pool->release_fb_cb = av1_release_frame_buffer;

if (av1_alloc_internal_frame_buffers(&pool->int_frame_buffers))
aom_internal_error(&pbi->error, AOM_CODEC_MEM_ERROR,
"Failed to initialize internal frame buffers");

pool->cb_priv = &pool->int_frame_buffers;
}

pool->get_fb_cb

1
2
3
4
5
6
7
static void *AllocWithGetFrameBufferCb(void *priv, size_t size) {
AllocCbParam *param = (AllocCbParam *)priv;
if (param->pool->get_fb_cb(param->pool->cb_priv, size, param->fb) < 0)
return NULL;
if (param->fb->data == NULL || param->fb->size < size) return NULL;
return param->fb->data;
}

析构函数也没有相应的检查,完全可能触发double free:

1
2
3
4
5
6
7
8
9
10
11
12
13
void av1_free_internal_frame_buffers(InternalFrameBufferList *list) {
int i;

assert(list != NULL);

for (i = 0; i < list->num_internal_frame_buffers; ++i) {
aom_free(list->int_fb[i].data);
list->int_fb[i].data = NULL;
}
aom_free(list->int_fb);
list->int_fb = NULL;
list->num_internal_frame_buffers = 0;
}

攻击

触发该 Double Free 需要满足:aom_calloc 返回 NULLav1_get_frame_buffer 返回 -1,解码器进入错误处理模式,调用 aom_codec_destroy,销毁流程进入 av1_free_internal_frame_buffers,对同一个 data 地址再次调用 free

但是因为calloc 很难真正返回 NULL,没法写poc。或者可以考虑使用libfiu (Fault Injection User-space):,这是一个专门用于模拟 POSIX 函数失败的库,来模拟调用 malloc/calloc 时返回失败。

修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int av1_get_frame_buffer(void *cb_priv, size_t min_size,
aom_codec_frame_buffer_t *fb) {
int i;
InternalFrameBufferList *const int_fb_list =
(InternalFrameBufferList *)cb_priv;
if (int_fb_list == NULL) return -1;

// Find a free frame buffer.
for (i = 0; i < int_fb_list->num_internal_frame_buffers; ++i) {
if (!int_fb_list->int_fb[i].in_use) break;
}

if (i == int_fb_list->num_internal_frame_buffers) return -1;

if (int_fb_list->int_fb[i].size < min_size) {
aom_free(int_fb_list->int_fb[i].data);
int_fb_list->int_fb[i].data = NULL; // 修复:确保在任何返回前指针为 NULL

int_fb_list->int_fb[i].data = (uint8_t *)aom_calloc(1, min_size);
if (!int_fb_list->int_fb[i].data) {
int_fb_list->int_fb[i].size = 0;
return -1;
}
int_fb_list->int_fb[i].size = min_size;
}

fb->data = int_fb_list->int_fb[i].data;
fb->size = int_fb_list->int_fb[i].size;
int_fb_list->int_fb[i].in_use = 1;

// Set the frame buffer's private data to point at the internal frame buffer.
fb->priv = &int_fb_list->int_fb[i];
return 0;
}

103:SWIG_JavaArrayOutInt

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/libwebp/swig/libwebp_java_wrap.c
函数:SWIG_JavaArrayOutInt
CWE:CWE-190 整数溢出

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
jintArray SWIG_JavaArrayOutInt (JNIEnv *jenv, int *result, jsize sz) {
jint *arr;
int i;
jintArray jresult = (*jenv)->NewIntArray(jenv, sz);
if (!jresult)
return NULL;
arr = (*jenv)->GetIntArrayElements(jenv, jresult, 0);
if (!arr)
return NULL;
for (i=0; i<sz; i++)
arr[i] = (jint)result[i];
(*jenv)->ReleaseIntArrayElements(jenv, jresult, arr, 0);
return jresult;
}

sz 是 jsize,是 有符号 32 位整数。NewIntArray 内部也会校验负数、过大值。这里没有整数相乘之类的算术运算,没有发生整数计算溢出

循环边界中,i 是 int,sz 是 jsize,没有隐式的 widening / narrowing,不存在 wraparound

这个函数中不存在CWE190漏洞

104:avif_context_free

目标路径:github/qt-avif-image-plugin/ext/libavif/contrib/gdk-pixbuf/loader.c
函数:avif_context_free
CWE:CWE-119,CWE-200

CWE-119:内存缓冲区范围内作的不当限制

该产品对内存缓冲区执行作,但它从缓冲区预期边界之外的内存位置读写。这可能导致对可能与其他变量、数据结构或程序内部数据关联的意外内存位置进行读写作。

CWE-120:信息泄露

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static void
avif_context_free (struct avif_context *context)
{
if (!context)
return;

if (context->decoder) {
avifDecoderDestroy (context->decoder);
context->decoder = NULL;
}

if (context->data) {
g_byte_array_unref (context->data);
context->bytes = NULL;
}

if (context->bytes) {
g_bytes_unref (context->bytes);
context->bytes = NULL;
}

if (context->pixbuf) {
g_object_unref (context->pixbuf);
context->pixbuf = NULL;
}

g_free (context);
}

分析

这里应该把context->data置NULL,结果却置了context->bytes。应该是笔误

1
2
3
4
if (context->data) {
g_byte_array_unref (context->data);
context->bytes = NULL;
}

如果 context->data 存在,context->bytes 会被提前置为 NULL。 紧接着的 if (context->bytes) 判断就会失效,导致原本应该释放的 GBytes 对象发生内存泄漏。 如果其他地方错误地引用了未释放或被错误置空的指针,可能诱发 Use-After-Free 或非法访问。

但是不存在CWE-119,没有越界写的可能。

也不存在信息泄露,函数中没有向外泄露的途径

105:av1_loop_filter_dealloc

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/av1/common/thread_common.c
函数:av1_loop_filter_dealloc
CWE:CWE-119

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
void av1_loop_filter_dealloc(AV1LfSync *lf_sync) {
if (lf_sync != NULL) {
int j;
#if CONFIG_MULTITHREAD
int i;
for (j = 0; j < MAX_MB_PLANE; j++) {
if (lf_sync->mutex_[j] != NULL) {
for (i = 0; i < lf_sync->rows; ++i) {
pthread_mutex_destroy(&lf_sync->mutex_[j][i]);
}
aom_free(lf_sync->mutex_[j]);
}
if (lf_sync->cond_[j] != NULL) {
for (i = 0; i < lf_sync->rows; ++i) {
pthread_cond_destroy(&lf_sync->cond_[j][i]);
}
aom_free(lf_sync->cond_[j]);
}
}
if (lf_sync->job_mutex != NULL) {
pthread_mutex_destroy(lf_sync->job_mutex);
aom_free(lf_sync->job_mutex);
}
#endif
aom_free(lf_sync->lfdata);
for (j = 0; j < MAX_MB_PLANE; j++) {
aom_free(lf_sync->cur_sb_col[j]);
}

aom_free(lf_sync->job_queue);
av1_zero(*lf_sync);
}
}

在函数执行的最后,使用av1_zero将整个 lf_sync 结构体的内容清零。av1_zero的定义是\#define av1_zero(dest) memset(&(dest), 0, sizeof(dest))。此外这个函数先销毁了cond_和mutex_,然后释放内部指针,最后清零结构体,确保了程序不会访问到已经标记为释放的不确定内存,所以不存在cwe119漏洞

107:highbd_fill_col_to_arr

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/av1/common/resize.c
函数:highbd_fill_col_to_arr
CWE:CWE-125 越界读取

1
2
3
4
5
6
7
8
9
static void highbd_fill_col_to_arr(uint16_t *img, int stride, int len,
uint16_t *arr) {
int i;
uint16_t *iptr = img;
uint16_t *aptr = arr;
for (i = 0; i < len; ++i, iptr += stride) {
*aptr++ = *iptr;
}
}

分析

这个函数完全依赖于调用者传入参数的正确性,没有任何边界检查。函数不知道 img 指向的内存块实际有多大。如果 len(高度)或 stride(步长)的组合超过了 img 实际分配的行数或内存范围,iptr 就会指向非法内存地址。

另外,如果 arr 的长度小于 len,就会发生越界写入。

但是在当前上下文中难以触发,因为:在 av1_resize_plane 函数中,arrbuf 的大小是根据输入参数动态分配的:

1
2
3
4
5
6
uint8_t *arrbuf = (uint8_t *)aom_malloc(sizeof(uint8_t) * height); // 分配 height 大小
// ...
for (i = 0; i < width2; ++i) {
fill_col_to_arr(intbuf + i, width2, height, arrbuf); // 传入的 len 参数正是 height
// ...
}

由于分配长度正好是 height,而 fill_col_to_arr 的循环次数也是 height。只要 aom_malloc 成功,aptr 的写入操作就永远处于 arrbuf 的边界内

同时,代码在关键位置设置了防护:

1
2
3
4
assert(width > 0);
assert(height > 0);
assert(width2 > 0);
assert(height2 > 0);

这防止了通过传入负数或零来诱发整数溢出或非法循环的行为

fill_col_to_arr 中,iptr += stride。在调用处,传入的 stridewidth2。 对应的 intbuf 分配大小为 width2 * height。因此循环最大索引为 (height - 1) * width2 + i,当 i(当前列)达到最大值 width2 - 1 时,最大访问地址为 (height - 1) * width2 + width2 - 1 = height * width2 - 1。这个地址正好是 intbuf 缓冲区的最后一个元素,不会越界

修复

虽然在当前文件中不会触发漏洞,但是还是建议修复。因为不知道具体大小,下面只是给出一个示例

1
2
3
4
5
6
7
8
9
10
11
static void highbd_fill_col_to_arr(uint16_t *img, int stride, int len,
uint16_t *arr) {
int i;
uint16_t *iptr = img;
uint16_t *aptr = arr;
if (len > arr_max_len) len = arr_max_len;
for (i = 0; i < len; ++i, iptr += stride) {
if (iptr > img_base + img_size) break;
*aptr++ = *iptr;
}
}

108:dav1dCodecDestroyInternal

目标路径:github/qt-avif-image-plugin/ext/libavif/src/codec_dav1d.c
函数:dav1dCodecDestroyInternal
CWE:CWE-362

分析

1
2
3
4
5
6
7
8
9
10
static void dav1dCodecDestroyInternal(avifCodec * codec)
{
if (codec->internal->hasPicture) {
dav1d_picture_unref(&codec->internal->dav1dPicture);
}
if (codec->internal->dav1dContext) {
dav1d_close(&codec->internal->dav1dContext);
}
avifFree(codec->internal);
}

检查 codec->internal->hasPicture和行使用 codec->internal->dav1dPicture这两步之间没有同步机制。同时Qt AVIF 插件明确支持多线程解码。

dav1dCodecGetNextImage() 中有:

1
codec->internal->hasPicture = AVIF_TRUE;

而在 dav1dCodecGetNextImage() 执行的同时,另一个线程可能调用 dav1dCodecDestroyInternal() 来销毁codec。

但是API 设计是单线程的,虽然内部(dav1d)使用多线程,但外部调用必须序列化。

1
2
3
4
5
6
7
8
9
// Qt AVIF 插件的使用方式 - 完全是单线程的
QAVIFHandler::ensureDecoder() {
m_decoder = avifDecoderCreate(); // 线程:主线程
avifDecoderParse(m_decoder); // 线程:主线程
}

QAVIFHandler::~QAVIFHandler() {
avifDecoderDestroy(m_decoder); // 线程:主线程
}

解码和销毁也是序列化的。所以在这个上下文里不会触发,或者需要用户代码完全违反 API 使用规范才会触发。所以不构成漏洞

109:

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/av1/common/restoration.c
函数:extend_frame_highbd
CWE:CWE-787 越界写

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void extend_frame_highbd(uint16_t *data, int width, int height,
ptrdiff_t stride, int border_horz,
int border_vert) {
uint16_t *data_p;
int i, j;
for (i = 0; i < height; ++i) {
data_p = data + i * stride;
for (j = -border_horz; j < 0; ++j) data_p[j] = data_p[0];
for (j = width; j < width + border_horz; ++j) data_p[j] = data_p[width - 1];
}
data_p = data - border_horz;
for (i = -border_vert; i < 0; ++i) {
memcpy(data_p + i * stride, data_p,
(width + 2 * border_horz) * sizeof(uint16_t));
}
for (i = height; i < height + border_vert; ++i) {
memcpy(data_p + i * stride, data_p + (height - 1) * stride,
(width + 2 * border_horz) * sizeof(uint16_t));
}
}

extend_frame_highbd 是 AOM 库内部的辅助函数,用于将帧边界扩展到指定宽度(border),以便后续的滤波或恢复操作不会访问无效内存。在 libavif 中,此函数被间接调用,用于处理 AVIF 图像的 YUV 平面数据。libavif 确保传入的帧尺寸和缓冲区大小是有效的(通过 avifImage 结构和像素格式检查)。

函数内部使用循环遍历帧的行和列,计算索引时依赖于传入的参数(如 stridewidthheightborder)。dst_leftdst_right 的计算基于 strideborder,但 AOM 确保缓冲区分配时已预留足够空间。libavif 在调用 AOM 前验证图像尺寸,避免了负索引或溢出。

函数不进行动态内存分配,依赖调用者提供的缓冲区。libavif 的高层 API确保帧缓冲区大小正确\

综上该函数不存在CWE787漏洞

110:arg_parse_enum_helper

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/common/args_helper.c
函数:arg_parse_enum_helper
CWE:CWE-824 未初始化指针访问

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int arg_parse_enum_helper(const struct arg *arg, char *err_msg) {
const struct arg_enum_list *listptr;
long rawval;
char *endptr;

if (err_msg) err_msg[0] = '\0';

rawval = strtol(arg->val, &endptr, 10);
if (arg->val[0] != '\0' && endptr[0] == '\0') {
for (listptr = arg->def->enums; listptr->name; listptr++)
if (listptr->val == rawval) return (int)rawval;
}

for (listptr = arg->def->enums; listptr->name; listptr++)
if (!strcmp(arg->val, listptr->name)) return listptr->val;

SET_ERR_STRING("Option %s: Invalid value '%s'\n", arg->name, arg->val);
return 0;
}

在函数定义中,rawvalendptr 只是被声明,并未赋初值。随后执行了rawval = strtol(arg->val, &endptr, 10);

这个函数的声明:

1
2
3
extern long int strtol (const char *__restrict __nptr,
char **__restrict __endptr, int __base)
__THROW __nonnull ((1));

执行完rawval = strtol(arg->val, &endptr, 10);之后rawvalendptr 是有值的

宏定义中也有对err_msg的检查

1
2
#define SET_ERR_STRING(...) \
  if (err_msg) snprintf(err_msg, ARG_ERR_MSG_MAX_LEN, __VA_ARGS__)

综上不存在未初始化指针访问问题

111:aom_yv12_realloc_with_new_border_c

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/aom_scale/generic/yv12extend.c
函数:aom_yv12_realloc_with_new_border_c
CWE:CWE-20 不充分的输入校验

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int aom_yv12_realloc_with_new_border_c(YV12_BUFFER_CONFIG *ybf, int new_border,
int byte_alignment, bool alloc_pyramid,
int num_planes) {
if (ybf) {
if (new_border == ybf->border) return 0;
YV12_BUFFER_CONFIG new_buf;
memset(&new_buf, 0, sizeof(new_buf));
const int error = aom_alloc_frame_buffer(
&new_buf, ybf->y_crop_width, ybf->y_crop_height, ybf->subsampling_x,
ybf->subsampling_y, ybf->flags & YV12_FLAG_HIGHBITDEPTH, new_border,
byte_alignment, alloc_pyramid, 0);
if (error) return error;
aom_yv12_copy_frame(ybf, &new_buf, num_planes);
aom_extend_frame_borders(&new_buf, num_planes);
aom_free_frame_buffer(ybf);
memcpy(ybf, &new_buf, sizeof(new_buf));
return 0;
}
return -2;
}

new_border 直接被传递给了 aom_alloc_frame_buffer。如果调用者传入了一个非法值,可能造成问题

查找调用,在av1_scale_references 函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
if ((ref->y_crop_width > cm->width || ref->y_crop_height > cm->height) &&
ref->border < AOM_BORDER_IN_PIXELS) {

RefCntBuffer *ref_fb = get_ref_frame_buf(cm, ref_frame);

if (aom_yv12_realloc_with_new_border(
&ref_fb->buf, AOM_BORDER_IN_PIXELS, // <--- 关键参数:new_border
cm->features.byte_alignment, cpi->alloc_pyramid,
num_planes) != 0) {
aom_internal_error(cm->error, AOM_CODEC_MEM_ERROR,
"Failed to allocate frame buffer");
}
}

这里new_border 传入的是宏 AOM_BORDER_IN_PIXELS,不会造成非法值注入。所以在这个上下文中不存在CWE-20问题。

112 :fold_mul_and_sum_rvv

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/aom_dsp/riscv/cdef_block_rvv.c
函数:fold_mul_and_sum_rvv
CWE:CWE-415 double free

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static inline vuint32m1_t fold_mul_and_sum_rvv(vint16m1_t partiala,
vint16m1_t partialb,
vuint32m1_t const1,
vuint32m1_t const2) {
vint32m2_t cost = __riscv_vwmul_vv_i32m2(partiala, partiala, 8);
cost = __riscv_vwmacc_vv_i32m2(cost, partialb, partialb, 8);

vuint32m2_t tmp1_u32m2 = __riscv_vreinterpret_v_i32m2_u32m2(cost);
vuint32m1_t cost_u32m1 = __riscv_vmul_vv_u32m1(
__riscv_vlmul_trunc_v_u32m2_u32m1(tmp1_u32m2), const1, 4);
tmp1_u32m2 = __riscv_vslidedown_vx_u32m2(tmp1_u32m2, 4, 8);
vuint32m1_t ret = __riscv_vmacc_vv_u32m1(
cost_u32m1, __riscv_vlmul_trunc_v_u32m2_u32m1(tmp1_u32m2), const2, 4);
return ret;
}

函数内部全部是算术运算指令(如乘法 vwmul、乘累加 vwmacc、滑动 vslidedown 等)。代码中没有任何对 free()等内存管理函数的调用,所以不存在double free风险

113 :av1_free_ref_frame_buffers

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/av1/common/alloccommon.c
函数:av1_free_ref_frame_buffers
CWE:CWE-662 同步不当

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void av1_free_ref_frame_buffers(BufferPool *pool) {
int i;

for (i = 0; i < pool->num_frame_bufs; ++i) {
if (pool->frame_bufs[i].ref_count > 0 &&
pool->frame_bufs[i].raw_frame_buffer.data != NULL) {
pool->release_fb_cb(pool->cb_priv, &pool->frame_bufs[i].raw_frame_buffer);
pool->frame_bufs[i].raw_frame_buffer.data = NULL;
pool->frame_bufs[i].raw_frame_buffer.size = 0;
pool->frame_bufs[i].raw_frame_buffer.priv = NULL;
pool->frame_bufs[i].ref_count = 0;
}
aom_free(pool->frame_bufs[i].mvs);
pool->frame_bufs[i].mvs = NULL;
aom_free(pool->frame_bufs[i].seg_map);
pool->frame_bufs[i].seg_map = NULL;
aom_free_frame_buffer(&pool->frame_bufs[i].buf);
}
aom_free(pool->frame_bufs);
pool->frame_bufs = NULL;
pool->num_frame_bufs = 0;
}

该函数直接对 pool->frame_bufs 进行循环操作并释放,存在竞争的可能。检查 ref_count > 0 到将其设为 0 之间是一段较长的时间窗,调用了release_fb_cb,并且缺乏原子操作,可能产生错误

虽然结构体支持”Shared by all the FrameWorkers”,但在 av1_dx_iface.c 的实现中,只创建了一个 frame_worker(第432行 aom_malloc(sizeof(*ctx->frame_worker))),所以不会有并发访问和修改buffer_pool,不存在同步问题

114 :arg_parse_enum_helper

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/common/args_helper.c
函数:arg_parse_enum_helper
CWE:CWE-20

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
int arg_parse_enum_helper(const struct arg *arg, char *err_msg) {
const struct arg_enum_list *listptr;
long rawval;
char *endptr;

if (err_msg) err_msg[0] = '\0';

rawval = strtol(arg->val, &endptr, 10);
if (arg->val[0] != '\0' && endptr[0] == '\0') {
for (listptr = arg->def->enums; listptr->name; listptr++)
if (listptr->val == rawval) return (int)rawval;
}

for (listptr = arg->def->enums; listptr->name; listptr++)
if (!strcmp(arg->val, listptr->name)) return listptr->val;

SET_ERR_STRING("Option %s: Invalid value '%s'\n", arg->name, arg->val);
return 0;
}

strtol 将字符串转换为 long 类型,但函数最后将结果强转为 int 并返回。

1
2
3
4
/* Convert a string to a long integer.  */
extern long int strtol (const char *__restrict __nptr,
char **__restrict __endptr, int __base)
__THROW __nonnull ((1));

代码没有检查 errno。如果输入是一个超出 long 范围的数字,strtol 会返回 LONG_MAX/MIN 并设置 errnoERANGE

如果系统上 long 是 64 位而 int 是 32 位,输入一个较大的数字(如 4294967297)会通过 rawval == listptr->val 的校验(如果 enum 定义也是 long),但在 return (int)rawval 时会发生高位截断,导致逻辑错误。

调用链:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
qt-avif-image-plugin/ext/libavif/ext/aom/apps/aomenc.c的int main(int argc, const char **argv_)
do {
stream = new_stream(&global, stream);
stream_cnt++;
if (!streams) streams = stream;
} while (parse_stream_params(&global, stream, argv));

static int parse_stream_params(struct AvxEncoderConfig *global,
struct stream_state *stream, char **argv){
...
set_config_arg_ctrls(config, ctrl_args_map[i], &arg);
}

static void set_config_arg_ctrls(struct stream_config *config, int key,
const struct arg *arg) {
...
if (key == AV1E_SET_TARGET_SEQ_LEVEL_IDX) {
config->arg_ctrls[j][0] = key;
config->arg_ctrls[j][1] = arg_parse_enum_or_int(arg);
}

int arg_parse_enum_or_int(const struct arg *arg)

int arg_parse_enum_or_int_helper(arg, err_msg)

int arg_parse_enum_helper(const struct arg *arg, char *err_msg)

int arg_parse_enum_helper(const struct arg *arg, char *err_msg)

所以可以通过命令行参数来直接注入,存在漏洞。比如:

1
2
3
4
# 传入一个超出 int 范围但在 long 范围内的值
./aomenc --target-seq-level-idx=4294967320 -o out.ivf ../../../../cat.y4m
# 构造极长字符串,尝试冲垮 SET_ERR_STRING 的 err_msg 缓冲区
./aomenc --target-seq-level-idx=$(python3 -c "print('A'*5000)") -o out.ivf ../../../../cat.y4m

攻击过程

尝试单独编译

aomenc.c 是 libaom 自带的一个独立命令行编码工具。得另外单独编译。这个过程很曲折,命令改来改去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
qt-avif-image-plugin/ext/libavif/ext/aom
mkdir -p build_test && cd build_test
cmake .. \
-DAOM_TARGET_CPU=generic \
-DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS="-fsanitize=address" \
-DCMAKE_CXX_FLAGS="-fsanitize=address" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address" \
-DCONFIG_AV1_ENCODER=1 \
-DCONFIG_AV1_DECODER=1 \
-DENABLE_TOOLS=ON \
-DENABLE_DOCS=OFF \
-DENABLE_EXAMPLES=ON \
-DENABLE_TESTS=OFF \
-DCONFIG_PIC=ON
make -j$(nproc) aomenc

image-20260126172804354

image-20260126193410903

攻击

唉,还是不得行。那没办法了,编写一个代码单独调用arg_parse_enum_helper来验证。

PoC代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 模拟 args.h 中的结构体定义
struct arg_enum_list {
const char *name;
int val;
};
struct arg_def {
const char *short_name;
const char *long_name;
int has_val;
const char *desc;
const struct arg_enum_list *enums;
};
struct arg {
char **argv;
const char *name;
const char *val;
const struct arg_def *def;
};

// 声明目标函数
extern int arg_parse_enum_helper(const struct arg *arg, char *err_msg);

int main() {
// 1. 准备枚举列表
struct arg_enum_list enums[] = { {"baseline", 0}, {NULL, 0} };
struct arg_def def = { NULL, "test", 1, "test desc", enums };

// 2. 构造超长字符串
// 假设 SET_ERR_STRING 内部缓冲区较小
char long_val[5000];
memset(long_val, 'A', 4999);
long_val[4999] = '\0';

struct arg my_arg = { NULL, "target-seq-level-idx", long_val, &def };
char err_msg[128]; // 模拟一个受限的错误消息缓冲区

printf("[*] Calling arg_parse_enum_helper with long string...\n");

// 执行漏洞函数
arg_parse_enum_helper(&my_arg, err_msg);

printf("[*] Returned successfully (No overflow detected).\n");
return 0;
}

编译poc

1
2
gcc -fsanitize=address -g arg_parse_enum_helper_poc.c ../common/args_helper.c \
-I.. -I../common -I. -I../common -o poc_vuln

然后运行

image-20260126194514383

ASan报告分析:

漏洞类型stack-buffer-overflow(栈溢出)。

触发位置../common/args_helper.c:178。这正是 arg_parse_enum_helper 函数中的 for (listptr = arg->def->enums; listptr->name; listptr++),说明程序尝试读取 listptr->name 时,访问了非法的内存地址

溢出原因:ASan 提示 Address 0x7ffff6650070 is located in stack... at offset 64 in frame,并指出溢出发生在变量 'my_arg' 上。这意味着poc中的 long_val(5000 字节的 ‘A’)在被函数处理时,导致解析逻辑越过了 struct arg 的边界,读取到了相邻的栈数据。

这是libaom 参数解析函数 arg_parse_enum_helper 存在输入验证不当导致的栈溢出。虽然没法直接使用aomenc进行漏洞验证,但是这个poc也足以证明此函数存在CWE20漏洞了

修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
int arg_parse_enum_helper(const struct arg *arg, char *err_msg) {
const struct arg_enum_list *listptr;
long rawval;
char *endptr;

if (err_msg) err_msg[0] = '\0';

errno = 0; // 清空errno
rawval = strtol(arg->val, &endptr, 10);

// 检查溢出和范围
if (errno == ERANGE || rawval < INT_MIN || rawval > INT_MAX) {
SET_ERR_STRING("Option %s: Value out of range '%s'\n", arg->name, arg->val);
return 0;
}

if (arg->val[0] != '\0' && endptr[0] == '\0') {
for (listptr = arg->def->enums; listptr->name; listptr++)
if (listptr->val == rawval) return (int)rawval;
}

for (listptr = arg->def->enums; listptr->name; listptr++)
if (!strcmp(arg->val, listptr->name)) return listptr->val;

SET_ERR_STRING("Option %s: Invalid value '%s'\n", arg->name, arg->val);
return 0;
}

115 :aom_lpf_horizontal_6_dual_neon

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/aom_dsp/arm/loopfilter_neon.c
函数:aom_lpf_horizontal_6_dual_neon
CWE:CWE-415 double frewe

分析

1
2
3
4
5
6
7
void aom_lpf_horizontal_6_dual_neon(
uint8_t *s, int pitch, const uint8_t *blimit0, const uint8_t *limit0,
const uint8_t *thresh0, const uint8_t *blimit1, const uint8_t *limit1,
const uint8_t *thresh1) {
aom_lpf_horizontal_6_neon(s, pitch, blimit0, limit0, thresh0);
aom_lpf_horizontal_6_neon(s + 4, pitch, blimit1, limit1, thresh1);
}

它执行的是内存读取与写入(对像素值进行滤波计算),完全不涉及内存的管理,所以不存在double free风险

116 :aom_var_2d_u16_c

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/aom_dsp/sum_squares.c
函数:aom_var_2d_u16_c
CWE:CWE-787 越界写

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
uint64_t aom_var_2d_u16_c(uint8_t *src, int src_stride, int width, int height) {
uint16_t *srcp = CONVERT_TO_SHORTPTR(src);
int r, c;
uint64_t ss = 0, s = 0;

for (r = 0; r < height; r++) {
for (c = 0; c < width; c++) {
const uint16_t v = srcp[c];
ss += v * v;
s += v;
}
srcp += src_stride;
}

return (ss - s * s / (width * height));
}

这个函数中,所有的操作都是读取,没有写入操作,所以不存在越界写问题。

但是函数对传入的参数 widthheightsrc_stride没有任何检查,有可能存在越界读、除零或者溢出风向

117 :aomCodecDestroyInternal

目标路径:github/qt-avif-image-plugin/ext/libavif/src/codec_aom.c
函数:aomCodecDestroyInternal
CWE:CWE-362

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
static void aomCodecDestroyInternal(avifCodec * codec)
{
#if defined(AVIF_CODEC_AOM_DECODE)
if (codec->internal->decoderInitialized) {
aom_codec_destroy(&codec->internal->decoder);
}
#endif

#if defined(AVIF_CODEC_AOM_ENCODE)
if (codec->internal->encoderInitialized) {
aom_codec_destroy(&codec->internal->encoder);
}
#endif

avifFree(codec->internal);
}

在函数外部有调用aom_codec_destroy进行销毁

1
2
aom_codec_destroy(&codec->internal->encoder);
codec->internal->encoderInitialized = AVIF_FALSE;

但是调用 aom_codec_destroy()后立即设置 codec->internal->encoderInitialized = AVIF_FALSE

而在aomCodecDestroyInternal 函数中会检查 if (codec->internal->encoderInitialized),由于已设置为 FALSE,不会再次调用 aom_codec_destroy()

这些操作都发生在同一个编码线程中,没有并发操作,所以不构成漏洞

118 :av1_cdef_dealloc_data

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/av1/encoder/pickcdef.c
函数:av1_cdef_dealloc_data
CWE:CWE-401 内存泄露

分析

1
2
3
4
5
6
7
8
9
10
void av1_cdef_dealloc_data(CdefSearchCtx *cdef_search_ctx) {
if (cdef_search_ctx) {
aom_free(cdef_search_ctx->mse[0]);
cdef_search_ctx->mse[0] = NULL;
aom_free(cdef_search_ctx->mse[1]);
cdef_search_ctx->mse[1] = NULL;
aom_free(cdef_search_ctx->sb_index);
cdef_search_ctx->sb_index = NULL;
}
}

这个函数调用 aom_free 释放了结构体中分配给 mse[0]mse[1]sb_index 的动态内存。在释放内存后,立即将指针置为 NULL,不存在内存泄露

120 :aom_yv12_realloc_with_new_border_c

目标路径:github/qt-avif-image-plugin/ext/libavif/ext/aom/aom_scale/generic/yv12extend.c
函数:aom_yv12_realloc_with_new_border_c
CWE:CWE-20

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
int aom_yv12_realloc_with_new_border_c(YV12_BUFFER_CONFIG *ybf, int new_border,
int byte_alignment, bool alloc_pyramid,
int num_planes) {
if (ybf) {
if (new_border == ybf->border) return 0;
YV12_BUFFER_CONFIG new_buf;
memset(&new_buf, 0, sizeof(new_buf));
const int error = aom_alloc_frame_buffer(
&new_buf, ybf->y_crop_width, ybf->y_crop_height, ybf->subsampling_x,
ybf->subsampling_y, ybf->flags & YV12_FLAG_HIGHBITDEPTH, new_border,
byte_alignment, alloc_pyramid, 0);
if (error) return error;
aom_yv12_copy_frame(ybf, &new_buf, num_planes);
aom_extend_frame_borders(&new_buf, num_planes);
aom_free_frame_buffer(ybf);
memcpy(ybf, &new_buf, sizeof(new_buf));
return 0;
}
return -2;
}

函数对传入对象 ybf 的状态验证不足,直接使用了 ybf 中的多个成员变量来分配新内存

调用:qt-avif-image-plugin/ext/libavif/ext/aom/av1/encoder/encoder_utils.c的av1_scale_references中

1
2
3
4
5
6
7
8
RefCntBuffer *ref_fb = get_ref_frame_buf(cm, ref_frame);
if (aom_yv12_realloc_with_new_border(
&ref_fb->buf, AOM_BORDER_IN_PIXELS,
cm->features.byte_alignment, cpi->alloc_pyramid,
num_planes) != 0) {
aom_internal_error(cm->error, AOM_CODEC_MEM_ERROR,
"Failed to allocate frame buffer");
}

但是ybf来源是get_ref_frame_buf,是经过初始化或者验证的,所以在当前代码中不会构成漏洞

项目:bayer2rgb

GitHub - jdthomas/bayer2rgb: Command line utility to convert bayer grid data to rgb data. Integrates with ImageMagick.

环境

依赖:vcpkg

1
2
3
# 克隆 vcpkg 到项目内部(路径必须匹配 CMakeLists.txt)
git clone https://github.com/Microsoft/vcpkg.git
./vcpkg/bootstrap-vcpkg.sh

image-20260126211225696

开启 ASan编译

1
2
3
4
5
mkdir build/ && cd build/
cmake .. -DCMAKE_BUILD_TYPE=Debug \
-DCMAKE_C_FLAGS="-fsanitize=address -g" \
-DCMAKE_EXE_LINKER_FLAGS="-fsanitize=address"
cmake --build .

106:ClearBorders_uint16

目标路径:github/bayer2rgb/src/bayer.c
函数:ClearBorders_uint16
CWE:CWE-787 越界写

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
void
ClearBorders_uint16(uint16_t * rgb, int sx, int sy, int w)
{
int i, j;

i = 3 * sx * w - 1;
j = 3 * sx * sy - 1;
while (i >= 0) {
rgb[i--] = 0;
rgb[j--] = 0;
}

int low = sx * (w - 1) * 3 - 1 + w * 3;
i = low + sx * (sy - w * 2 + 1) * 3;
while (i > low) {
j = 6 * w;
while (j > 0) {
rgb[i--] = 0;
j--;
}
i -= (sx - 2 * w) * 3;
}

}
1
2
3
4
5
6
i = 3 * sx * w - 1;
j = 3 * sx * sy - 1;
while (i >= 0) {
rgb[i--] = 0; // 如果 sx 或 w 为 0,i 最初为 -1,循环不执行,看似安全
rgb[j--] = 0; // 致命伤:j 可能为 -1 或更小
}

这个循环只检查 i >= 0。如果 j 的计算值比 i 小(例如 sy < w),那么当 i 还大于 0 时,j 已经变成负数了。程序会向 rgb[-1]rgb[-2] 等地址写入 0,直接破坏 rgb 指针之前的内存(可能是栈帧数据、函数返回地址或堆管理元数据)。

1
2
int low = sx * (w - 1) * 3 - 1 + w * 3;
i = low + sx * (sy - w * 2 + 1) * 3;

这段计算完全没有边界检查。如果输入参数 sx(宽)、sy(高)、w(边界宽度)组合异常:sy - w * 2 + 1 可能为负数。整个 i 的初值可能远超 rgb 数组的实际大小。最后i 可能指向数组末尾之外的任何地方。

调用1:

1
2
3
4
5
dc1394error_t
dc1394_bayer_HQLinear_uint16(const uint16_t *B2R_RESTRICT bayer, uint16_t *B2R_RESTRICT rgb, int sx, int sy, int tile, int bits)
{
...
ClearBorders_uint16(rgb, sx, sy, 2);

调用2:

1
2
3
4
5
dc1394error_t
dc1394_bayer_EdgeSense_uint16(const uint16_t *B2R_RESTRICT bayer, uint16_t *B2R_RESTRICT rgb, int sx, int sy, int tile, int bits)
{
...
ClearBorders_uint16(rgb, sx, sy, 3);

这两个函数共同的引用:

1
2
3
4
5
6
7
8
dc1394error_t
dc1394_bayer_decoding_16bit(const uint16_t *B2R_RESTRICT bayer, uint16_t *B2R_RESTRICT rgb, uint32_t sx, uint32_t sy, dc1394color_filter_t tile, dc1394bayer_method_t method, uint32_t bits)
{
...
case DC1394_BAYER_METHOD_EDGESENSE:
return dc1394_bayer_EdgeSense_uint16(bayer, rgb, sx, sy, tile, bits);
case DC1394_BAYER_METHOD_HQLINEAR:
return dc1394_bayer_HQLinear_uint16(bayer, rgb, sx, sy, tile, bits);

最后在bayer2rgb/src/bayer2rgb.c的main函数中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int
main( int argc, char ** argv )
{
while ((c=getopt_long(argc,argv,"i:o:w:v:b:f:m:ths",longopt,&optidx)) != -1)
{
switch ( (char)c )
{
case 'i':
infile = strdup( optarg );
break;
case 'o':
outfile = strdup( optarg );
break;
case 'w':
width = strtol( optarg, NULL, 10 );
break;
case 'v':
height = strtol( optarg, NULL, 10 );
break;
...
if(tiff)
{
rgb_start = put_tiff(rgb, width, height, bpp);
}
...
dc1394_bayer_decoding_16bit((const uint16_t*)bayer, (uint16_t*)rgb_start, width, height, first_color, method, bpp);
break;
}

攻击

注意到主函数中还有输入参数验证,不能用height = 0了可惜

1
2
3
4
5
6
7
  // arguments: infile outfile width height bpp first_color
if( infile == NULL || outfile == NULL || bpp == 0 || width == 0 || height == 0 )
{
printf("Bad parameter\n");
usage(argv[0]);
return 1;
}

先搞一个raw文件放进build目录

命令:

1
./bayer2rgb -w 1 -v 1 -m HQLINEAR -b 16 -i input.raw -o output.tiff

结果:

image-20260126215041745

根据 width=1, height=1 分配的 rgb 缓冲区非常小。main 函数只检查了 != 0,没有验证最小尺寸是否符合算法要求(对于 HQLINEAR,width 至少应为 2×w=4)。ClearBorders_uint16 缺乏防御性编程,盲目信任传入的 sx, sy。最终导致了越界写入

修复

1
2
3
4
5
6
7
void ClearBorders_uint16(uint16_t * rgb, int sx, int sy, int w) {
// 增加此防御逻辑
if (sx < 2 * w || sy < 2 * w) return;

int i, j;
// ... 原有逻辑 ...
}

项目:webrtc_apm

GitHub - xia-chu/webrtc_apm: webrtc中apm相关代码的提取,包括AEC/NS/AGC/VAD ,另外还包括mp3/aac编码器、SoundTouch

119 :WindowAndFFT

目标路径:github/webrtc_apm/apm/src/main/jni/webrtc/webrtc/modules/audio_processing/aecm/aecm_core_c.c
函数:WindowAndFFT
CWE:CWE-190 整数溢出

分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static void WindowAndFFT(AecmCore* aecm,
int16_t* fft,
const int16_t* time_signal,
ComplexInt16* freq_signal,
int time_signal_scaling) {
int i = 0;

for (i = 0; i < PART_LEN; i++) {
int16_t scaled_time_signal = time_signal[i] << time_signal_scaling;
fft[i] = (int16_t)((scaled_time_signal * WebRtcAecm_kSqrtHanning[i]) >> 14);
scaled_time_signal = time_signal[i + PART_LEN] << time_signal_scaling;
fft[PART_LEN + i] = (int16_t)((
scaled_time_signal * WebRtcAecm_kSqrtHanning[PART_LEN - i]) >> 14);
}

WebRtcSpl_RealForwardFFT(aecm->real_fft, fft, (int16_t*)freq_signal);
for (i = 0; i < PART_LEN; i++) {
freq_signal[i].imag = -freq_signal[i].imag;
}
}

time_signalint16_t,其范围是 [−32768,32767]。如果 time_signal_scaling 大于 0,且原始信号值较大,左移操作会直接导致高位丢失。

1
int16_t scaled_time_signal = time_signal[i] << time_signal_scaling;

两个 int16_t 相乘的结果最大可达 230 左右。虽然 C 语言在计算中间值时可能会提升到 int32,但如果编译器环境或优化处理不当,或者 scaled_time_signal 已经是溢出后的极端值,这里的乘法结果可能超出预期。

1
fft[i] = (int16_t)((scaled_time_signal * WebRtcAecm_kSqrtHanning[i]) >> 14);

如果 freq_signal[i].imag 的值正好是 int16_t 的最小值 -32768 (0x8000)。对 -32768 取反(-)的结果依然是 -32768。这可能导致后续复数运算逻辑出现严重的数学错误。

1
freq_signal[i].imag = -freq_signal[i].imag;

调用路径: WebRtcAecm_ProcessFrameWebRtcAecm_ProcessBlockTimeToFrequencyDomainWindowAndFFT

1
2
3
4
5
6
7
8
9
10
11
12
13
static int TimeToFrequencyDomain(AecmCore* aecm,
const int16_t* time_signal,
ComplexInt16* freq_signal,
uint16_t* freq_signal_abs,
uint32_t* freq_signal_sum_abs) {
...

#ifdef AECM_DYNAMIC_Q
tmp16no1 = WebRtcSpl_MaxAbsValueW16(time_signal, PART_LEN2);
time_signal_scaling = WebRtcSpl_NormW16(tmp16no1);
#endif

WindowAndFFT(aecm, fft, time_signal, freq_signal, time_signal_scaling);

为了在 16 位定点运算中保持最高精度,AECM 会检测当前音频块的最大值,计算它距离溢出还有多少空间(NormW16),然后得到一个缩放因子 time_signal_scaling。这个缩放因子被作为参数传给 WindowAndFFT

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
int WebRtcAecm_ProcessBlock(AecmCore* aecm,
const int16_t* farend,
const int16_t* nearendNoisy,
const int16_t* nearendClean,
int16_t* output) {
int i;

uint32_t xfaSum;
uint32_t dfaNoisySum;
uint32_t dfaCleanSum;
uint32_t echoEst32Gained;
uint32_t tmpU32;

int32_t tmp32no1;

uint16_t xfa[PART_LEN1];
uint16_t dfaNoisy[PART_LEN1];
uint16_t dfaClean[PART_LEN1];
uint16_t* ptrDfaClean = dfaClean;
const uint16_t* far_spectrum_ptr = NULL;

// 32 byte aligned buffers (with +8 or +16).
// TODO(kma): define fft with ComplexInt16.
int16_t fft_buf[PART_LEN4 + 2 + 16]; // +2 to make a loop safe.
int32_t echoEst32_buf[PART_LEN1 + 8];
int32_t dfw_buf[PART_LEN2 + 8];
int32_t efw_buf[PART_LEN2 + 8];

int16_t* fft = (int16_t*) (((uintptr_t) fft_buf + 31) & ~ 31);
int32_t* echoEst32 = (int32_t*) (((uintptr_t) echoEst32_buf + 31) & ~ 31);
ComplexInt16* dfw = (ComplexInt16*)(((uintptr_t)dfw_buf + 31) & ~31);
ComplexInt16* efw = (ComplexInt16*)(((uintptr_t)efw_buf + 31) & ~31);

int16_t hnl[PART_LEN1];
int16_t numPosCoef = 0;
int16_t nlpGain = ONE_Q14;
int delay;
int16_t tmp16no1;
int16_t tmp16no2;
int16_t mu;
int16_t supGain;
int16_t zeros32, zeros16;
int16_t zerosDBufNoisy, zerosDBufClean, zerosXBuf;
int far_q;
int16_t resolutionDiff, qDomainDiff, dfa_clean_q_domain_diff;

const int kMinPrefBand = 4;
const int kMaxPrefBand = 24;
int32_t avgHnl32 = 0;

// Determine startup state. There are three states:
// (0) the first CONV_LEN blocks
// (1) another CONV_LEN blocks
// (2) the rest

if (aecm->startupState < 2)
{
aecm->startupState = (aecm->totCount >= CONV_LEN) +
(aecm->totCount >= CONV_LEN2);
}
// END: Determine startup state

// Buffer near and far end signals
memcpy(aecm->xBuf + PART_LEN, farend, sizeof(int16_t) * PART_LEN);
memcpy(aecm->dBufNoisy + PART_LEN, nearendNoisy, sizeof(int16_t) * PART_LEN);
if (nearendClean != NULL)
{
memcpy(aecm->dBufClean + PART_LEN,
nearendClean,
sizeof(int16_t) * PART_LEN);
}

// Transform far end signal from time domain to frequency domain.
far_q = TimeToFrequencyDomain(aecm,
aecm->xBuf,
dfw,
xfa,
&xfaSum);

// Transform noisy near end signal from time domain to frequency domain.
zerosDBufNoisy = TimeToFrequencyDomain(aecm,
aecm->dBufNoisy,
dfw,
dfaNoisy,
&dfaNoisySum);
aecm->dfaNoisyQDomainOld = aecm->dfaNoisyQDomain;
aecm->dfaNoisyQDomain = (int16_t)zerosDBufNoisy;


if (nearendClean == NULL)
{
ptrDfaClean = dfaNoisy;
aecm->dfaCleanQDomainOld = aecm->dfaNoisyQDomainOld;
aecm->dfaCleanQDomain = aecm->dfaNoisyQDomain;
dfaCleanSum = dfaNoisySum;
} else
{
// Transform clean near end signal from time domain to frequency domain.
zerosDBufClean = TimeToFrequencyDomain(aecm,
aecm->dBufClean,
dfw,
dfaClean,
&dfaCleanSum);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
int WebRtcAecm_ProcessFrame(AecmCore* aecm,
const int16_t* farend,
const int16_t* nearendNoisy,
const int16_t* nearendClean,
int16_t* out) {
int16_t outBlock_buf[PART_LEN + 8]; // Align buffer to 8-byte boundary.
int16_t* outBlock = (int16_t*) (((uintptr_t) outBlock_buf + 15) & ~ 15);

int16_t farFrame[FRAME_LEN];
const int16_t* out_ptr = NULL;
int size = 0;

// Buffer the current frame.
// Fetch an older one corresponding to the delay.
WebRtcAecm_BufferFarFrame(aecm, farend, FRAME_LEN);
WebRtcAecm_FetchFarFrame(aecm, farFrame, FRAME_LEN, aecm->knownDelay);

// Buffer the synchronized far and near frames,
// to pass the smaller blocks individually.
WebRtc_WriteBuffer(aecm->farFrameBuf, farFrame, FRAME_LEN);
WebRtc_WriteBuffer(aecm->nearNoisyFrameBuf, nearendNoisy, FRAME_LEN);
if (nearendClean != NULL)
{
WebRtc_WriteBuffer(aecm->nearCleanFrameBuf, nearendClean, FRAME_LEN);
}

// Process as many blocks as possible.
while (WebRtc_available_read(aecm->farFrameBuf) >= PART_LEN)
{
int16_t far_block[PART_LEN];
const int16_t* far_block_ptr = NULL;
int16_t near_noisy_block[PART_LEN];
const int16_t* near_noisy_block_ptr = NULL;

WebRtc_ReadBuffer(aecm->farFrameBuf, (void**) &far_block_ptr, far_block,
PART_LEN);
WebRtc_ReadBuffer(aecm->nearNoisyFrameBuf,
(void**) &near_noisy_block_ptr,
near_noisy_block,
PART_LEN);
if (nearendClean != NULL)
{
int16_t near_clean_block[PART_LEN];
const int16_t* near_clean_block_ptr = NULL;

WebRtc_ReadBuffer(aecm->nearCleanFrameBuf,
(void**) &near_clean_block_ptr,
near_clean_block,
PART_LEN);
if (WebRtcAecm_ProcessBlock(aecm,
far_block_ptr,
near_noisy_block_ptr,
near_clean_block_ptr,
outBlock) == -1)
{
return -1;
}

怎么触发:

  • 攻击者或特定的音频源提供一个极高频的纯音信号。FFT 变换后,某个频点的虚部 imag 恰好计算为 −32768 (0x8000)。执行 -(-32768) 在 16 位有符号数中结果依然是 −32768。

  • 如果 int 被视为 16 位,或者编译器在处理 int16 * int16 时没有正确提升到 32 位,两者相乘会瞬间超过 16 位范围。

如何从外部触发漏洞?由于这些函数处理的是从 farend(远端语音)和 nearend(近端语音)读入的数据,这意味着得构造具有特定频率分布和振幅的音频,诱导 NormW16 计算出极端的缩放因子,或者直接导致 FFT 输出边界值。

攻击

由于这个输入构造太过复杂,还是写一个调用这个函数的代码

编译时出现了各种报错,总结一下大概是重复定义、默认根目录、依赖和依赖的依赖问题。就不全放了

image-20260126230501124

总之没招了,决定把这个函数单独提出来进行验证。结果:

image-20260126230704548

PoC代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
#include <stdio.h>
#include <stdint.h>
#include <string.h>

// 定义 WebRTC 的复数结构体
typedef struct {
int16_t real;
int16_t imag;
} ComplexInt16;

#define PART_LEN 64

// 从 aecm_core_c.c 中提取的目标函数 (去掉了 static)
void WindowAndFFT_Isolated(int16_t* fft,
const int16_t* time_signal,
ComplexInt16* freq_signal,
int time_signal_scaling) {
int i = 0;
// 模拟 WebRTC 的 Hanning 窗系数 (Q14)
const int16_t WebRtcAecm_kSqrtHanning_Mock = 16384;

for (i = 0; i < PART_LEN; i++) {
// 风险点 1: 左移溢出
int16_t scaled_time_signal = time_signal[i] << time_signal_scaling;
fft[i] = (int16_t)((scaled_time_signal * WebRtcAecm_kSqrtHanning_Mock) >> 14);

scaled_time_signal = time_signal[i + PART_LEN] << time_signal_scaling;
fft[PART_LEN + i] = (int16_t)((scaled_time_signal * WebRtcAecm_kSqrtHanning_Mock) >> 14);
}

// 此处原本调用 WebRtcSpl_RealForwardFFT,为了证明 CWE-190,我们直接模拟 FFT 后的结果
// 假设 FFT 后的虚部恰好产生了一个临界值 -32768
freq_signal[10].imag = -32768;

// 风险点 2: 取反溢出 (CWE-190)
for (i = 0; i < PART_LEN; i++) {
freq_signal[i].imag = -freq_signal[i].imag;
}
}

int main() {
int16_t time_signal[PART_LEN * 2];
int16_t fft[PART_LEN * 2];
ComplexInt16 freq_signal[PART_LEN];
memset(freq_signal, 0, sizeof(freq_signal));

printf("--- WebRTC WindowAndFFT CWE-190 Vulnerability Proof ---\n\n");

// --- 测试 1: 左移溢出 ---
time_signal[0] = 16385; // 0x4001
int scaling = 1;
// 调用函数的前半部分逻辑
int16_t scaled = (int16_t)(time_signal[0] << scaling);
printf("[Test 1] 原始信号: %d, 左移因子: %d, 处理后信号: %d\n",
time_signal[0], scaling, scaled);
if (scaled < 0) {
printf(">> 结论: 正数变负数,触发符号位翻转!\n\n");
}

// --- 测试 2: 取反溢出 (核心 CWE-190) ---
// 模拟 FFT 输出了一个边界值
WindowAndFFT_Isolated(fft, time_signal, freq_signal, 0);

printf("[Test 2] 取反前的虚部: -32768\n");
printf("[Test 2] 取反后的虚部: %d\n", freq_signal[10].imag);

if (freq_signal[10].imag == -32768) {
printf(">> 结论: -(-32768) 依然等于 -32768!成功触发整数溢出 (CWE-190)。\n");
}

return 0;
}

目标函数是直接搬过来的,只修改其参数。能够证明该函数存在CWE-190漏洞

修复

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
static void WindowAndFFT_Fixed(AecmCore* aecm,
int16_t* fft,
const int16_t* time_signal,
ComplexInt16* freq_signal,
int time_signal_scaling) {
int i = 0;

for (i = 0; i < PART_LEN; i++) {
// 修复 1: 使用饱和左移防止符号翻转
int16_t scaled_time_signal = WebRtcSpl_SatShiftW16(time_signal[i], time_signal_scaling);
fft[i] = (int16_t)((scaled_time_signal * WebRtcAecm_kSqrtHanning[i]) >> 14);

scaled_time_signal = WebRtcSpl_SatShiftW16(time_signal[i + PART_LEN], time_signal_scaling);
fft[PART_LEN + i] = (int16_t)((scaled_time_signal * WebRtcAecm_kSqrtHanning[PART_LEN - i]) >> 14);
}

WebRtcSpl_RealForwardFFT(aecm->real_fft, fft, (int16_t*)freq_signal);

for (i = 0; i < PART_LEN; i++) {
// 修复 2: 使用饱和减法防止取反溢出
freq_signal[i].imag = WebRtcSpl_SubSatW16(0, freq_signal[i].imag);
}
}