- FFmpeg ซึ่งใช้ประมวลผลสื่อในเบราว์เซอร์และโครงสร้างพื้นฐานสตรีมมิงทั่วโลก เป็นพื้นผิวการโจมตีที่มีความสำคัญต่อความปลอดภัย เพราะต้องพาร์สอินพุตที่ซับซ้อนและไม่น่าเชื่อถือ
- เอเจนต์ความปลอดภัย อัตโนมัติของ depthfirst ค้นพบซีโร่เดย์ 21 รายการในโค้ด C ที่ผ่านการปรับแต่งอย่างหนักราว 1.5 ล้านบรรทัด โดยมีค่าใช้จ่ายประมาณ $1k หรือราว 10% ของค่าใช้จ่าย $10k ที่ Anthropic ใช้กับ Mythos
- สิ่งที่ค้นพบกระจายอยู่ในหลายคอมโพเนนต์ เช่น TS demuxer, VP9 decoder และเส้นทางการประมวลผล RTP/RTSP/RTMP โดยช่องโหว่บางรายการแฝงอยู่มานานถึง 15–20 ปี
- ช่องโหว่ใน AV1 RTP depacketizer นำไปสู่ PoC ที่เขียนทับ function pointer ได้ด้วย RTP packet ขนาดเพียง 183 ไบต์ และเข้าถึงได้เพียงแค่รัน
ffmpeg -i rtsp://attacker/stream
- การตรวจสอบความปลอดภัยที่ใช้งานได้จริงต้องไปไกลกว่าคำเตือนเชิงทฤษฎี โดยต้องมีอินพุตที่ทำซ้ำได้และยืนยันการทำงานจริง และการวิเคราะห์แบบเอเจนต์สามารถนำมาใช้ค้นหาช่องโหว่ที่ซ่อนอยู่ในโค้ดเบส C ขนาดใหญ่ได้โดยตรง
ภาพรวม
- FFmpeg เป็นซอฟต์แวร์ที่ถูกใช้งานอย่างแพร่หลายสำหรับประมวลผลสื่อในเบราว์เซอร์และโครงสร้างพื้นฐานของแพลตฟอร์มสตรีมมิงขนาดใหญ่
- เนื่องจากเป็นไลบรารีที่ต้องพาร์สสื่อที่ซับซ้อนและไม่น่าเชื่อถืออย่างต่อเนื่อง จึงมีความสำคัญด้านความปลอดภัยและเป็นเป้าหมายหลักของการโจมตีแบบ zero-click
- รีโพซิทอรีของ FFmpeg ประกอบด้วยโค้ด C ที่ผ่านการปรับแต่งอย่างหนักประมาณ 1.5 ล้านบรรทัด และพาร์สรูปแบบสื่อซับซ้อนหลายร้อยชนิด
- FFmpeg ผ่านการ fuzzing และการตรวจสอบด้วยมือมานานกว่า 20 ปี และล่าสุดทีม Google Big Sleep ได้เปิดเผย ช่องโหว่ 13 รายการ ใน FFmpeg
- Anthropic สแกน FFmpeg ด้วยโมเดล Mythos และพบ ปัญหาด้านความปลอดภัยบางส่วน
- หลังจากงานก่อนหน้าเหล่านี้ การค้นหาช่องโหว่ใหม่ใน FFmpeg ก็ยากขึ้น และกลายเป็นเป้าหมายสำหรับพิสูจน์ความสามารถของระบบแบบเอเจนต์ที่สแกนโค้ดเบสขนาดใหญ่ได้อย่างลึกซึ้ง
เอเจนต์ความปลอดภัยของ Depthfirst
- เอเจนต์เขียนโค้ดและเอเจนต์ความปลอดภัยอาจใช้โมเดลพื้นฐานเดียวกันได้ แต่เป้าหมายแตกต่างกันมาก
- เอเจนต์เขียนโค้ด มักรับโจทย์จากมนุษย์และมุ่งเขียนโค้ดแอปพลิเคชัน
- เอเจนต์ความปลอดภัย มีบทบาทที่แคบและมุ่งเป้าชัดเจนกว่า คือค้นหาปัญหาความปลอดภัยที่สามารถถูกใช้โจมตีได้จริงในระบบที่มีอยู่ โดยไม่มีคำสั่งเฉพาะเจาะจง
- เอเจนต์ความปลอดภัยต้องทำความเข้าใจสถาปัตยกรรมของโค้ดเบส, parser และ protocol handler ที่เปิดรับภายนอก, รวมถึงจุดที่อินพุตซึ่งผู้โจมตีควบคุมได้ไหลเข้าสู่ระบบก่อน
- จากนั้นจะไม่ปฏิบัติต่อรีโพซิทอรีเป็นเพียงกองไฟล์แบบแบน ๆ แต่จะติดตาม การไหลของข้อมูล จากโค้ดในพื้นผิวการโจมตีไปยังคอมโพเนนต์ที่เกี่ยวข้อง
- เอเจนต์ความปลอดภัยที่ใช้งานได้จริงต้องมี guardrail เพื่อไม่ให้จินตนาการเงื่อนไขที่ขาดหายไป ขยายความบั๊กเชิงทฤษฎีเกินจริง หรือสร้าง false positive จำนวนมาก
- ต้องตรวจสอบว่าผู้โจมตีควบคุมอินพุตที่ถูกต้องได้จริงหรือไม่ เข้าถึงเส้นทางที่มีช่องโหว่ได้จริงหรือไม่ และข้อบกพร่องที่สงสัยสามารถทำซ้ำได้หรือไม่
- เมื่อจำเป็น ต้องระบุหรือสร้าง harness สำหรับโต้ตอบกับคอมโพเนนต์เป้าหมายเพื่อทดสอบสมมติฐานให้เป็นรูปธรรม
- เอเจนต์ความปลอดภัยเฉพาะทางของ depthfirst วิเคราะห์โค้ดอย่างลึก และแตกแขนงสมมติฐานหลายชุดไปทดสอบแบบขนาน
- ผลลัพธ์ที่ได้ไม่ใช่รายงานเชิงทฤษฎีหรือคำเตือนกำกวม แต่เป็นปัญหาความปลอดภัยที่ยืนยันการทำงานจริงด้วยอินพุตเฉพาะที่ทำซ้ำได้
ผลการค้นพบ
- เอเจนต์ค้นพบ 21 ซีโร่เดย์ ครอบคลุมตั้งแต่ TS demuxer ไปจนถึง VP9 decoder
- ค่าใช้จ่ายรวมอยู่ที่ประมาณ $1k ซึ่งเป็นเพียงราว 10% ของค่าใช้จ่ายที่ Anthropic ใช้กับ Mythos
- การเปรียบเทียบค่าใช้จ่าย: {b:1,10}
-
ช่องโหว่ที่ได้รับการจัดสรร CVE
- CVE-2026-39210 คือ heap buffer overflow ใน TS demuxer ที่ขาดการตรวจขอบเขตความยาวก่อนอ่าน 2 ไบต์ และถูกเพิ่มเข้ามาในปี 2010
- CVE-2026-39211 คือ integer overflow จากสูตรตัวคูณขนาดที่ไม่มีขอบเขตบนระหว่างการรีแฟกเตอร์ swscale ทำให้พารามิเตอร์ที่ผู้ใช้ควบคุมสามารถก่อให้เกิดการสเกลขนาดใหญ่ตามอำเภอใจได้ และถูกเพิ่มเข้ามาในปี 2010
- CVE-2026-39212 คือ stack overflow ใน
ffmpeg_opt.c ที่ไฟล์ preset สามารถกระตุ้นการพาร์สออปชันแบบเรียกซ้ำได้โดยไม่มีการจำกัดความลึก และเป็น regression ที่เข้ามาในเดือนกรกฎาคม 2025
- CVE-2026-39213 คือ heap buffer overflow ในเส้นทางอินพุต rawvideo ของ yuv4mpegenc ที่ขาดการตรวจสอบมิติเทียบกับขนาด packet และถูกเพิ่มเข้ามาในปี 2023
- CVE-2026-39214 คือ stack buffer overflow ที่ implementation เดิมของ SDT เขียน service entry โดยไม่ติดตามพื้นที่ที่เหลือ และถูกเพิ่มเข้ามาตั้งแต่ปี 2003 โดยแฝงอยู่นาน 23 ปี
- CVE-2026-39215 คือ heap buffer overflow จากข้อผิดพลาดทางตรรกะใน
update_mb_info() ที่ทำให้การเรียกภายหลังเขียนเกินท้ายบัฟเฟอร์ที่จัดสรรไว้ 12 ไบต์ และถูกเพิ่มเข้ามาในปี 2012
- CVE-2026-39216 คือ heap buffer overflow ใน
img2enc.c ที่เกิดจากการแทนที่ขนาด chroma ที่ปลอดภัยด้วยขนาดไม่จำกัดตามมิติ และถูกเพิ่มเข้ามาในปี 2012
- CVE-2026-39217 คือ heap buffer overflow ใน VP9 decoder ที่เกิดจากฟังก์ชันอัปเดตขนาดที่ถูกรีแฟกเตอร์ ทำให้ tile thread buffer พลาดการ realloc ที่จำเป็น และเป็น regression ที่เข้ามาในเดือนมีนาคม 2025
- CVE-2026-39218 คือ heap buffer overflow ใน DASH demuxer ที่ไม่ปฏิเสธค่า duration ติดลบ ทำให้ดัชนีของ fragment array กลายเป็นค่าติดลบ และถูกเพิ่มเข้ามาในปี 2017
-
ช่องโหว่ที่อ้างอิงด้วยรหัสติดตามภายใน
- DFVULN-127 คือ heap buffer overflow ที่
av1_handle_packet() ของ RTP AV1 depacketizer ข้าม Temporal Delimiter OBU และเลื่อนตำแหน่งเอาต์พุตตาม obu_size แต่ไม่ได้จัดสรรพื้นที่ขนาดเดียวกัน ทำให้ OBU ถัดไปเขียนออกนอกขอบบัฟเฟอร์
- DFVULN-126 คือ heap buffer overflow ที่
run_legacy_unscaled() ในโค้ด swscale graph จัดการการแปลง interlaced YUV420P→NV12 ผิดพลาด จนเขียนเกิน Y-plane ปลายทาง 576 ไบต์
- DFVULN-125 คือ stack buffer overflow ที่
jpeg_create_header() ของ RTP JPEG depacketizer สร้างส่วน quantization-table ลงใน stack buffer 1024 ไบต์ และเมื่อ qtable_len >= 1024 ค่า AV_WB16 หลัง packet จะเขียนเกินท้ายไป 2 ไบต์
- DFVULN-124 คือ heap buffer overflow ที่
istg_parse_tile_grid() ในเส้นทาง AVIF overlay ไม่ปฏิเสธการอ้างอิง dimg ที่มี tile entry เป็น 0 รายการ ทำให้หลัง unsigned wrap เกิดการอ่านนอกขอบเขตจาก heap allocation ขนาด 1 ไบต์
- DFVULN-123 คือ integer overflow ที่
latm_parse_packet() ของ RTP LATM depacketizer ใช้ signed 32-bit addition overflow เพื่อหลบการตรวจขอบเขต ทำให้ memcpy อ่านเลยท้าย heap buffer ไปราว 1GB
- DFVULN-122 คือ heap buffer overflow ที่
aac_parse_packet() ของ RTP MPEG-4 depacketizer ยอมรับ AU-headers-length เป็น 0 จนเกิดการจัดสรร 1 ไบต์ แล้วอ่านเป็นฟิลด์ 4 ไบต์โดยไม่ตรวจว่ามี AU header อยู่จริง
- DFVULN-121 คือ heap buffer underflow ที่
read_seek() ของ CAF demuxer ไม่ตรวจค่าที่ av_index_search_timestamp() คืนมาเป็น -1 ก่อนใช้เป็นดัชนีอาร์เรย์ ทำให้เข้าถึง index_entries[-1]
- DFVULN-120 คือ integer underflow ที่
ff_read_riff_info() ของ AVI demuxer รับ size - 4 โดยไม่ตรวจ size >= 4 ทำให้ LIST chunk ขนาด 0 underflow เป็นราว 4GB และนำไปสู่การจัดสรรราว 2GB
- DFVULN-119 คือ heap buffer overflow ที่การเพิ่มค่าเกินจำเป็นใน
opt_map() ของ option parser ทำให้ parse link-label เป็น file index ผิดพลาด และเก็บ stream index -1 จนลูปภายหลังอ่านก่อนหน้าอาร์เรย์ AVStream**
- DFVULN-118 คือ heap buffer overflow ที่
rtsp_read_announce() ในเส้นทาง RTSP server จัดการ Content-Length ติดลบว่าใช้ได้ ทำให้ ANNOUNCE ระยะไกลพร้อม Content-Length: -1 เขียนออกนอกขอบเขตที่ sdp[-1]
- DFVULN-117 คือ heap buffer overflow ที่
rtmp_calc_swfhash() ของ RTMP client ตรวจเพียง in_size < 3 แทน in_size < 8 ทำให้อ่าน 8 ไบต์จากบัฟเฟอร์ขั้นต่ำ 3 ไบต์
- DFVULN-116 คือ heap buffer overflow ที่
sdp_parse_line() ในการพาร์ส RTSP SDP คำนวณ strlen(control_url) - 1 บนสตริงว่าง ทำให้ size_t wrap เป็น SIZE_MAX และเกิดการอ่าน pre-buffer 1 ไบต์
จากตัวมาร์กเกอร์ของเฟรมที่ถูกข้ามไปสู่การควบคุม PC
- ในบรรดารายการที่ค้นพบทั้ง 21 รายการ มี heap buffer overflow ใน AV1 RTP depacketizer ที่เข้าถึงได้ผ่านเครือข่ายโดยไม่ต้องใช้แฟลกพิเศษใด ๆ
- เหยื่อเพียงแค่รัน
ffmpeg -i rtsp://attacker/stream และ packet เดียวขนาด 183 ไบต์ ก็สามารถเปลี่ยนทิศทางการทำงานได้
- เมื่อ FFmpeg ดึง RTSP stream เซิร์ฟเวอร์จะส่งวิดีโอที่เข้ารหัสมาเป็นลำดับของ RTP packet
- AV1 จัดบิตสตรีมเป็น OBU (Open Bitstream Units) และ RTP payload format จะแบ่ง OBU เหล่านี้ออกเป็นหลาย packet
- depacketizer ของ FFmpeg ทำหน้าที่นำ OBU ที่ถูกแบ่งกลับมาต่อเป็น elementary stream ที่สะอาดอีกครั้ง
- Temporal Delimiter (TD) เป็นมาร์กเกอร์ขนาดเล็กที่คั่นหนึ่ง temporal unit หรือก็คือระหว่างเฟรมหนึ่งกับเฟรมถัดไป
- สเปกระบุว่า depacketizer ต้อง “ignore and remove” TD ที่อยู่ใน payload
- กระบวนการ “ignore and remove” นี้เองกลายเป็นจุดที่มีปัญหา และเอเจนต์ตรวจพบจุดดังกล่าว
สาเหตุราก
- depacketizer สร้าง output packet แบบค่อยเป็นค่อยไป โดยมีเคอร์เซอร์
pktpos ติดตามตำแหน่งไบต์ถัดไปที่จะเขียนใน pkt->data
pktpos เริ่มต้นจากท้ายปัจจุบันของ packet
// libavformat/rtpdec_av1.c:199
pktpos = pkt->size;
- เมื่อโค้ดวนผ่าน OBU element ใน packet ทุกไบต์ที่มีการส่งออกจริงจะมีการเรียก
av_grow_packet นำหน้า ซึ่งจะขยาย heap allocation ของ pkt->data
- invariant ที่ทั้งรูทีนนี้พึ่งพาคือ
pktpos ต้องไม่วิ่งนำหน้าขนาดที่จัดสรรไว้ของ pkt->data
- โค้ดจัดการ Temporal Delimiter ทำลาย invariant นี้
// libavformat/rtpdec_av1.c:250
if ((obu_type == AV1_OBU_TEMPORAL_DELIMITER) ||
(obu_type == AV1_OBU_TILE_LIST)) {
pktpos += obu_size;
rem_pkt_size -= obu_size;
obu_cnt++;
continue;
}
- เมื่อข้าม TD ค่า
pktpos จะถูกเลื่อนไปข้างหน้าตาม obu_size ที่ผู้โจมตีประกาศไว้ แต่ไม่ได้มีการจัดสรรหน่วยความจำมารองรับการเลื่อนนั้น
- ตัวชี้อินพุต
buf_ptr ก็ไม่ได้เลื่อนผ่านไบต์ของ TD ด้วย
- หาก TD มี
obu_size = 148 ค่า pktpos จะกลายเป็น 148 แต่ pkt->data อาจยังไม่ได้ถูกจัดสรรเลยหรือยังเล็กกว่า 148 ไบต์มาก
- การวนรอบถัดไปจะพาร์สไบต์ของ TD เดิมซ้ำอีกครั้ง อ่านไบต์ header ของ TD ใหม่เป็นความยาว OBU ใหม่ และใช้ payload เป็นเนื้อหาของ OBU ที่ถูกปรับแต่ง
- เมื่อเจอ OBU ปกติถัดไป packet จะถูกขยายเพิ่มเพียงเท่าขนาดของ OBU นั้น
// libavformat/rtpdec_av1.c:296
if ((result = av_grow_packet(pkt, output_size)) < 0)
return result;
// libavformat/rtpdec_av1.c:304 / 336
pkt->data[pktpos++] = *buf_ptr++ | AV1F_OBU_HAS_SIZE_FIELD;
memcpy(pkt->data + pktpos, buf_ptr, obu_payload_size);
- หาก OBU ที่ถูกปรับแต่งมีขนาด 17 ไบต์
av_grow_packet จะจัดสรรบัฟเฟอร์ 81 ไบต์ โดยรวม 17 ไบต์กับ input padding 64 ไบต์ของ FFmpeg
- การเขียนจะเริ่มที่
pkt->data[148] ซึ่งอยู่เลยท้ายการจัดสรรไป 67 ไบต์
- ข้อบกพร่องนี้จึงกลายเป็น heap buffer overflow ที่ผู้โจมตีควบคุมได้ทั้งออฟเซ็ตและเนื้อหาที่เขียนทับ
วิธีการโจมตี
- เพื่อให้ overflow ที่ควบคุมได้มีประโยชน์ จะต้องมีเป้าหมายที่เขียนทับได้อยู่ถัดจากบัฟเฟอร์ และ allocator ของ FFmpeg ก็จัดวางเป้าหมายดังกล่าวไว้พอดี
- เมื่อ
av_grow_packet จัดสรร packet data buffer มันจะผ่าน av_buffer_alloc ซึ่งฟังก์ชันนี้จะจัดสรร data buffer, โครงสร้าง bookkeeping AVBuffer และ AVBufferRef ต่อกันบน heap
- FFmpeg ใช้
posix_memalign แบบจัดแนว 64 ไบต์ ดังนั้น data buffer ขนาด 81 ไบต์จะครอบครอง chunk ขนาด 128 ไบต์ และ struct AVBuffer จะถูกวางถัดไปทันที
- ใน struct
AVBuffer มี function pointer ที่ใช้เป็น free callback
// libavutil/buffer_internal.h
struct AVBuffer {
uint8_t *data; // +0
size_t size; // +8
atomic_uint refcount; // +16
void (*free)(void *opaque, uint8_t *data); // +24
void *opaque; // +32
...
};
- เมื่ออิงจากจุดเริ่มต้นของ data buffer ตัวชี้
AVBuffer.free อยู่ที่ออฟเซ็ต 152
- หาก TD มี
obu_size = 148 การเขียนจะเริ่มที่ pkt->data[148]
- ไบต์ header ของ TD คือ
0x10 จะถูกตีความใหม่เป็นความยาว 16 และ header กับ payload ของ OBU ขนาด 16 ไบต์ที่ถูกปรับแต่งจะถูกเขียนตั้งแต่ออฟเซ็ต 148
AVBuffer.refcount อยู่ที่ออฟเซ็ต 144–147 จึงยังคงอยู่ก่อนตำแหน่งเริ่มเขียนที่ 148 และรักษาค่าเดิม 1 ไว้
- เอ็กซ์พลอยต์ฝัง OBU ที่ถูกปรับแต่งตัวที่สามไว้ใน TD payload เพื่อบังคับให้เกิด
av_grow_packet อีกครั้ง
- เนื่องจากบัฟเฟอร์ถูกสร้างด้วย
av_buffer_alloc ไม่ใช่ av_buffer_realloc มันจึงไม่ถูกทำเครื่องหมายว่า reallocatable และ FFmpeg จะเลือกเส้นทางที่จัดสรรบัฟเฟอร์ใหม่ก่อน แล้วค่อยปล่อยบัฟเฟอร์เก่า
// libavutil/buffer.c:209
if (!(buf->buffer->flags_internal & BUFFER_FLAG_REALLOCATABLE) || ...) {
ret = av_buffer_realloc(&new, size);
memcpy(new->data, buf->data, ...);
buffer_replace(pbuf, &new);
return 0;
}
buffer_replace จะลด refcount ของบัฟเฟอร์เดิมจาก 1 เป็น 0 แล้วเรียก free callback
// libavutil/buffer.c:129
if (atomic_fetch_sub_explicit(&b->refcount, 1, memory_order_acq_rel) == 1) {
b->free(b->opaque, b->data);
}
- ณ จุดนี้ free pointer ที่ถูกเขียนทับจะถูกเรียกใช้ และทำให้ควบคุม instruction pointer ได้
- ใน release build RTP packet ขนาด 183 ไบต์เพียง packet เดียวทำให้
rip กลายเป็น 0xdeadbeef
#0 0x00000000deadbeef in ?? ()
rip 0xdeadbeef 0xdeadbeef
#1 buffer_replace (buffer.c:133)
#2 av_buffer_realloc (buffer.c:220)
#3 av_grow_packet (packet.c:151)
#4 av1_handle_packet (rtpdec_av1.c:296)
#5 rtp_parse_packet_internal (rtpdec.c:743)
ขอบเขตผลกระทบและ PoC
- สภาพแวดล้อมที่ทำให้ FFmpeg เปิด RTSP URL ที่ผู้โจมตีมีอิทธิพลได้ จะได้รับผลกระทบจากช่องโหว่นี้
- media ingest pipeline ที่ดึง stream URL จากผู้ใช้ อยู่ในขอบเขตผลกระทบ
- ระบบ surveillance และ CCTV ที่ดึง RTSP feed อยู่ในขอบเขตผลกระทบ
- transcoding service ที่ประมวลผลแหล่ง AV1-over-RTP ระยะไกล อยู่ในขอบเขตผลกระทบ
- ไม่ต้องมีการยืนยันตัวตนหรือการโต้ตอบจากผู้ใช้นอกเหนือจากการเปิด stream และไม่ต้องใช้ command-line flag แปลก ๆ
- ช่องโหว่นี้ถูกกระตุ้นในขั้นตอน RTSP
PLAY ปกติที่ไคลเอนต์เหล่านี้ทำอยู่แล้วตามการออกแบบ
- โค้ด PoC อยู่ที่ ffmpeg-dfvuln127
1 ความคิดเห็น
ความคิดเห็นจาก Hacker News
FFmpeg มีประวัติด้านความปลอดภัยที่แย่ผิดปกติ
เป็นเวลานานมาแล้วที่ทุกครั้งซึ่งรัน fuzzer ก็มักเจอบั๊ก memory corruption โผล่มาแทบไม่รู้จบ และเมื่อ 10 ปีก่อนก็เคยมีงานจากพนักงาน Google ด้วย: https://security.googleblog.com/2014/01/ffmpeg-and-thousand-...
ดังนั้นแม้นี่จะเป็นเดโมที่แสดงความสามารถของ LLM แต่ก็ไม่ใช่เรื่องน่าแปลกใจนัก ถ้าคุณกำลังจัดการคอนเทนต์ที่ไม่น่าเชื่อถือหรือผู้ใช้เป็นคนส่งมา ก็ไม่ควร รัน FFmpeg นอก sandbox และการทำแบบนั้นคือการยอมรับความเสี่ยงที่ไม่สมเหตุสมผล
มีช่องโหว่อยู่ใน codec เฉพาะทางมาก ๆ ที่เหมือนจะมีใช้แค่ในวิดีโอเกมยุค 90 เกมหนึ่งเท่านั้น ผู้รายงานทำเหมือนเป็นเรื่องใหญ่ แต่พวกเขาพูดทำนองว่ามันไม่ใช่เรื่องสำคัญเพราะเป็น codec ที่แทบไม่มีใครใช้
แต่ก็อดสงสัยไม่ได้ว่าพวกเขาไม่รู้หรืออย่างไรว่า ถ้าผู้โจมตีสามารถส่งไฟล์วิดีโอมาได้ ก็เลือก video codec อะไรก็ได้ตามใจชอบ ต่อให้นักพัฒนาคิดว่า codec นั้นไม่มีใครใช้เลย ถ้ามันยังใช้งานได้ ผู้โจมตีก็ยังใช้มันได้
เลยสงสัยว่าตัวเองพลาดอะไรไปหรือไม่ หรือมีเหตุผลที่สมควรจริง ๆ ที่ทำให้ช่องโหว่ของ codec นี้ไม่ใช่เรื่องใหญ่
จนกระทั่งไม่นานมานี้ยังไม่มี virtio GPU native context จึงแทบเป็นไปไม่ได้เลยที่จะ sandbox วิดีโอเพลเยอร์โดยไม่เสีย hardware acceleration ทั้งหมด อย่างน้อยถ้าจะบังคับจากภายนอกก็ทำได้ยาก ส่วนภายในนั้นสามารถแยก FFmpeg ออกมาและล็อกด้วย seccomp อย่างเข้มงวดแบบ Chromium ได้
นี่ไม่ใช่ปัญหาเฉพาะของ FFmpeg เท่านั้น Apple เองก็มีช่องโหว่ในตัวถอดรหัสภาพและวิดีโอนับไม่ถ้วน สาขานี้มันยากมากโดยตัวมันเอง และ FFmpeg ก็ทำงานได้ครอบคลุมกว่าทุกเจ้า
ผมคิดว่าอุตสาหกรรมนี้กำลัง optimize ผิดจุด มันง่ายมากที่จะใช้ของอย่าง Mythos preview 1 หรือ GPT-5.5 สร้างบั๊กรายงานที่ AI เขียนขึ้นมาหลายพันรายการ สิ่งที่ยากคือ การแก้บั๊กจริง ๆ
ช่วงไม่กี่เดือนมานี้ผมกำลังสร้างระบบที่เปิด PR แทนการแค่หาประเด็นความปลอดภัยร้ายแรงแล้วส่งรายงานเข้ามา จนถึงตอนนี้อัตราการยอมรับอยู่ที่ประมาณ 94% สาเหตุที่ไม่ผ่านส่วนใหญ่ไม่ใช่เพราะวิเคราะห์ช่องโหว่ผิด แต่เป็นเพราะ kill switch เฉพาะโปรเจกต์หรือกลไกภายในที่ไม่มีการทำเอกสารไว้
ดูเหมือนนักพัฒนาส่วนใหญ่จะชอบวิธีนี้มากกว่า รายงานบั๊กคือการสร้างงานเพิ่ม ส่วน PR ที่ดีคือการลดงานลง ฟังดูชัดเจนอยู่แล้ว แต่สินค้าด้านความปลอดภัยจำนวนมากก็ยังหยุดอยู่แค่การส่งรายงานแล้วจบ
มันเป็นแบบนี้มาตลอดตั้งแต่อุตสาหกรรมนี้ถือกำเนิดขึ้น และตอนนี้เพิ่งเริ่มมีเครื่องมือที่เหมาะสมสำหรับประเมินความเสียหายและความเปราะบางโดยรวม
บั๊กนี้ร้ายแรงเพราะระยะการเข้าถึง ทุก deployment ที่ให้ FFmpeg ไปแตะ RTSP URL ที่อยู่ภายใต้อิทธิพลของผู้โจมตีล้วนเสี่ยง
ไม่ว่าจะเป็นมีเดียอินเจสต์ไปป์ไลน์ที่รับ stream URL จากผู้ใช้ ระบบเฝ้าระวังหรือ CCTV ที่ดึง RTSP feed หรือบริการทรานส์โค้ดที่ประมวลผลแหล่ง AV1-over-RTP จากระยะไกล ทั้งหมดนี้เข้าข่าย มันร้ายแรงพอตัวจริง ๆ และถึงขั้นน่าแปลกใจที่มีการเปิดเผยออกมา ผมนึกบริการหลายอย่างที่ดูเหมือนถูกนำไปใช้โจมตีได้ทันทีออกเลย
ต่อให้เรื่องนี้จะดูเหมือนโฆษณาของบริษัทความปลอดภัย และอาจไม่ได้ใหญ่โตอย่างที่ทำให้ดู ก็ยังเตือนให้เห็นว่าที่ไหนสักแห่งในทุกแอปพลิเคชันที่คุณปล่อยออกมา ย่อมมีช่องโหว่ความปลอดภัยอยู่ และตอนนี้แม้แต่ script kiddie ก็ใช้ เครดิต 2 ดอลลาร์ หาเจอได้ภายใน 5 นาทีหลังปล่อย
ถ้าคุณไม่ให้ red team ตรวจโค้ดก่อนปล่อย แฮกเกอร์ก็จะทำแทนหลังปล่อย
คำว่า zero-day กำลังถูกใช้เกินจริง ช่องโหว่ที่อธิบายไว้ไม่มีอันไหนเป็น zero-day จริง แต่พอเรียกแบบนั้นมันก็ดูเท่และเรียกคลิกได้ดี
ประโยคที่ว่า “ณ จุดนี้ free pointer ที่เสียหายถูกเรียกใช้ และสิทธิ์ควบคุม instruction pointer ก็ตกมาอยู่ในมือเรา” นั้นร้ายแรงมาก
แต่อย่างไรก็ดี ดูเหมือนในทางปฏิบัติบั๊กนี้เพียงอย่างเดียวจะยังไม่พาไปถึง remote code execution ตามอำเภอใจได้ โดยเฉพาะเมื่อมี ASLR และยังต้องมีหน้าเมมโมรีที่ทั้งเขียนได้และรันได้อยู่ที่ไหนสักแห่งด้วย
ถึงอย่างนั้นก็ยังต้องมี exploit อื่นมาใช้เพื่อเจาะ ASLR อยู่ดี
นั่นไม่ใช่ความหมายของคำว่า “zero-day”
โครงสร้างแรงจูงใจในแวดวง การวิจัยด้านความปลอดภัย พังเสียหายอย่างหนัก
คนพวกนี้เหมือนผู้จัดการระดับกลางในโลก FOSS คอยโยนงานเพิ่มให้เหล่าอาสาสมัครแล้วได้รับคำชื่นชม และยิ่งงานนั้นเร่งด่วนมากก็ยิ่งได้รับคำชมมากขึ้น การยอมรับผลกระทบที่แท้จริงหรือข้อพึงพิจารณาเชิงปฏิบัติของประเด็นนั้นกลับขัดกับแรงจูงใจของพวกเขา
มันยากที่จะไม่มองว่าพวกเขาเป็นเหมือนคนงานเก็บกวาดชั้นใต้ดินของอุตสาหกรรมซอฟต์แวร์ และถึงเวลาแล้วที่น่าจะเริ่มปฏิบัติกับพวกเขาเหมือนคนนอกคอก ส่ง PR มาสิ ไม่งั้นก็เงียบไปเสีย
ฉันเองก็ใช้ FFmpeg มานานมาก ทั้งส่วนตัวและในบริการที่ฉันสร้างขึ้นมา Fabrice Bellard คืออัจฉริยะ และนักพัฒนาที่พา FFmpeg มาถึงจุดนี้ก็ทำให้โลกอุดมสมบูรณ์ขึ้นอย่างวัดได้
แต่เมื่อรันกับอินพุตที่เชื่อถือไม่ได้ ก็แทบนึกไม่ออกเลยว่าจะมีโปรแกรมไหนที่ควรค่าแก่การ sandbox มากไปกว่า FFmpeg เพราะมันคือโค้ด C ขนาดมหึมาที่ต้องจัดการกับวิดีโอ·ออดิโอ codec ที่ซับซ้อนและขึ้นชื่อว่าเป็นเรื่องยากมากที่จะทำให้ถูกต้องสมบูรณ์
ถึงอย่างนั้น ในทางปฏิบัติมันก็ไม่ได้เป็นปัญหาใหญ่นัก รัน FFmpeg ใน VM หรือ gVisor ได้อยู่แล้ว และผลลัพธ์สุดท้ายก็มักเป็นไฟล์วิดีโอที่คุณตั้งใจจะเล่นในเบราว์เซอร์อยู่ดี ซึ่งในเบราว์เซอร์ก็จะถูกถอดรหัสอีกทีภายใน sandbox อีกชั้นหนึ่ง ดังนั้นนี่จึงเป็นงานที่ยากมาตั้งแต่ต้น
การทำ sandbox ที่ปลอดภัยมักกลายเป็นโอกาสให้เกิดการคัดลอกแบบไม่จำกัดได้ง่าย
เป็นช่องโหว่ที่ในโค้ด reverse packetization ของ RTP LATM นั้น
latm_parse_packet()ทำการบวก signed 32-bit แล้วเกิด overflow จนข้ามการตรวจสอบขอบเขตได้เป็นช่องโหว่อีกครั้งที่เกิดจากการบวกโดยไม่มีการตรวจสอบ และถึงอย่างนั้นภาษาแบบสมัยใหม่อย่าง Rust หรือ Go ก็ไม่ได้โยนข้อยกเว้นเมื่อ overflow ขณะที่สถาปัตยกรรม CPU สมัยใหม่อย่าง RISC-V ก็ไม่ได้มี overflow trap ให้ ส่วนภาษาเก่าอย่าง C หรือ C++ ก็แน่นอนว่าไม่มีการตรวจสอบ overflow เช่นกัน
มันน่าขันดี เห็นได้ชัดว่าเราไม่อาจเชื่อได้ว่ามนุษย์จะเขียนโค้ดเลขคณิตได้ถูกต้องเสมอ
พฤติกรรม overflow ของจำนวนเต็มในโหมดรีลีสของ Rust ก็ถูกกำหนดไว้ชัดเจนเช่นกัน คือมันจะ wrap เท่านั้น จึงมีโอกาสนำไปสู่ช่องโหว่น้อยลง แน่นอนว่าถ้าเริ่มใช้ unsafe Rust ก็เป็นอีกเรื่อง
Rust ไม่ได้โยนข้อยกเว้นจาก overflow เป็นค่าเริ่มต้น แต่ก็เขียนแบบ 123.checked_add(321) ได้ แบบนั้นโค้ดจะอ่านยากขึ้น แต่ปลอดภัยจาก overflow
พูดตามตรง จากวิธีที่ฉันเขียนโค้ด ฉันว่ารูปแบบอย่างคอมเมนต์ท้ายบรรทัดน่าจะดีกว่า เช่น
var x = y + z; # wrappedโอกาสที่จะผสมเลขคณิตแบบ wrapping·checked·saturating หลายแบบในบรรทัดเดียวมีน้อยมาก ใน Zig ทุกบรรทัดต้องสามารถคอมไพล์ได้ด้วยตัวเองโดยไม่ต้องอาศัยบริบทโค้ดอื่น ดังนั้นสถานะของคอมไพเลอร์แบบ
doing(wrapped) { x + y }จึงใช้ไม่ได้ ชื่อฟังก์ชันก็ยาวเกินไป และการแปลงชนิดก็ยาวเกินไป ตัวแก้ไขระดับ statement อาจเป็นทางประนีประนอมที่ดี