← ALL RESEARCH
Field Notes

CVE 두 개를 받기까지 — minimp3·dr_libs 퍼징과 수기 원인 분석

CVE-2026-37715 · CVE-2026-37716 · minimp3 / dr_libs (dr_wav) · 2026-03-14
TL;DR
이 글의 순서
  1. 시작하며
  2. 도구 스택
  3. 결함 1 — dr_wav 힙 오버플로우
  4. 결함 2 — minimp3 정수 오버플로우
  5. 벤더 공조 & 타임라인
  6. 어려웠던 점
  7. 배운 점
  8. 마치며

시작하며

"오래된 헤더온리 라이브러리들은 새 퍼저를 돌려도 결과가 안 나오는 영역이 많지 않을까"라는 생각에서 시작했다. minimp3나 dr_libs 같은 single-file drop-in 라이브러리는 한 번 빌드에 들어가면 업스트림 쪽에서 잘 안 쳐다본다. 근데 그 안에서 처리하는 데이터는 사용자가 주는 파일이다 — 공격 표면은 그대로 살아있는데 관심만 없는 셈이다. 실제로 minimp3는 dr_mp3·miniaudio·raylib 같은 다수 게임/미디어 라이브러리에 임베드되어 있어서, 여기서 뭔가 나오면 파급이 크다.

도구 스택

libFuzzer + AddressSanitizer + UndefinedBehaviorSanitizer 조합으로 coverage-guided 퍼징을 돌렸다. 타겟 식별 → LLVMFuzzerTestOneInput harness 작성 → 기존 WAV/MP3 샘플로 seed corpus 구성 → ASan/UBSan을 켠 clang 빌드 → 코퍼스 늘려가며 fuzz 실행. 크래시가 뜨는 것까지는 퍼저가 데려다주는데, 왜 터졌는지는 거기서부터 순전히 내 몫이다. ASan/UBSan 로그를 코드 라인까지 따라가서 원인을 밝히고, 최소 재현 PoC를 만들고, 기존에 올라와 있던 비슷한 이슈들과 겹치지 않는지 구분하는 것까지가 한 세트다.

결함 1 — dr_wav: Two-Pass 파서가 어긋나면서 생기는 힙 오버플로우

dr_wav(v0.14.5)는 메타데이터를 두 번 훑는다. 1차 패스(counting)에서 얼마나 큰 버퍼가 필요한지 세고, 2차 패스(reading)에서 실제로 그 버퍼에 데이터를 채워 넣는다. 문제는 drwav_init__internal (dr_wav.h:3326)이 fmt 청크의 header.sizeInBytes를 검증하지 않고 무조건 16바이트를 읽는다는 것이었다.

dr_wav.h:3326
pWav->onRead(pWav->pUserData, fmtData, sizeof(fmtData)); // 항상 16바이트 읽음

sizeInBytes == 0인 fmt 청크를 주면 1차 패스와 2차 패스가 같은 바이트를 서로 다르게 해석하게 된다.

Pass 1 — counting fmt(0B)를 16B로 읽음 → 커서 밀림 뒷 데이터를 엉뚱한 chunk로 오인식 → 버퍼 크기 계산: 작게(과소 할당) Pass 2 — reading fmt(0B)를 이번엔 맞게 처리 뒷 데이터를 큰 chunk로 해석 → 큰 데이터를 그대로 write 과소 할당된 버퍼에 큰 데이터 write → Heap Overflow
sizeInBytes=0 하나가 두 패스의 해석을 어긋나게(desync) 만드는 흐름

두 패스가 같은 입력을 다르게 해석하면서 어긋나는(desync) 게 이 버그의 본질이다.

디버그 빌드에서는 drwav__metadata_get_memory (dr_wav.h:2130)에서 assertion이 걸려서 바로 티가 나지만, 릴리스 빌드는 조용히 그냥 overflow가 나버린다. 그리고 overflow되는 크기와 그 자리에 써지는 내용 둘 다 공격자가 제어할 수 있다.

PoC (55바이트) — sizeInBytes=0 하나로 트리거된다.

crash.wav (hex)
RIFF 2500 0000 57415645 # RIFF + size=37 + WAVE 666d7420 # "fmt " 00000000 # sizeInBytes = 0 ← 트리거 0000010044ac000044ac0000 01000800 6461746101000000 # "data" chunk 0000000000000080

재현은 drwav_init_memory_with_metadata 하나로 끝난다.

test.c
drwav wav; drwav_init_memory_with_metadata(&wav, buf, sz, 0, NULL); drwav_uninit(&wav); // clang -fsanitize=address,undefined -g -O1 -o test test.c -lm

같은 코드베이스에 있던 CVE-2026-29022(smpl chunk mismatch)와는 다른 chunk, 다른 케이스라는 것도 확인해서 리포트에 같이 적었다 — 중복 리포트가 아니라는 걸 스스로 증명해야 했다.

결함 2 — minimp3: 검증 안 된 필드 하나가 10TB 할당 요청이 되는 과정

mp3dec_check_vbrtag(minimp3_ex.h:229)가 Xing/Info VBR 헤더에서 frame count를 읽는 줄이 있다.

minimp3_ex.h:229
*frames = (uint32_t)(tag[0] << 24) | (tag[1] << 16) | (tag[2] << 8) | tag[3];

tag[0]은 uint8_t인데 shift 연산 전에 signed int로 promotion된다. tag[0] >= 128이면 tag[0] << 24가 int 범위를 넘어가서 C11 §6.5.7 위반 — undefined behavior다. UBSan이 정확히 이 줄을 짚어줬다:

UBSan
runtime error: left shift of 255 by 24 places cannot be represented in type 'int'

여기서 끝이 아니고, 이렇게 나온 frames 값(최대 약 42억)이 그 뒤로 아무 검증 없이 흘러간다.

detected_samples = samples * (uint64_t)frames // ≈ 4.9조 malloc(detected_samples * sizeof(mp3d_sample_t)) // ≈ 10TB
ASan
requested allocation size 0x8fff7132900 (~10TB) exceeds maximum supported size

VBR 헤더 몇 바이트 조작만으로 프로세스가 10TB 할당을 시도하다가 죽는다 — DoS로는 충분한 임팩트다.

같은 라이브러리에 있던 Issue #122(다른 위치의 shift UB)나 Issue #134(같은 함수의 다른 버그 — heap over-read)와도 겹치지 않는, minimp3에 대한 첫 CVE였다.

PoC는 여기 안 올린다. 이 건은 3월에 이슈를 연 뒤로 지금(2026-09)까지 메인테이너 응답이 없고, 코드도 아직 패치가 안 됐다. 사이트 원칙상 패치 전 취약점은 재현 코드·페이로드를 공개하지 않는다 — 실제로 트리거되는 값의 정확한 구성은 패치되면 마저 공개하겠다.

벤더 공조 & 타임라인

두 이슈 다 2026년 3월 14일에 같은 날 열었다. dr_wav 쪽은 메인테이너 mackron이 4월 25일에 바로 마스터 브랜치에 수정 커밋(0100187)을 올리고 검증을 요청해왔고, 확인해준 뒤 5월 11일에 이슈가 완료(completed)로 닫혔다. minimp3 쪽은 그 뒤로 지금까지 댓글이 0개다. 같은 날 연 두 이슈가 이렇게 갈릴 수 있다는 것도 이번에 알았다 — 벤더 반응 속도는 순전히 메인테이너 개인 여력에 달려있다. CVE 자체는 둘 다 MITRE에 예약(RESERVED)된 채로, 공식 발행은 아직이다. minimp3 이슈는 그 뒤로 다른 리서처들이 올린 관련 발견(#142, #145, #147, #150)이 줄줄이 달리면서 스레드만 길어지고 있다 — 다들 같은 코드를 보고 있다는 뜻이니 이쪽은 조만간 누군가는 고칠 것 같다.

어려웠던 점

퍼저가 크래시를 던져주는 것과, 그게 "진짜 취약점"인지 아는 건 다른 문제였다. ASan 로그 하나 보고 리포트부터 쓰면 안 된다 — dr_wav 건은 two-pass 구조 자체를 이해하기 전까지는 그냥 "어딘가 overflow 남" 정도로만 보였다. Pass 1과 Pass 2가 같은 입력을 각각 어떻게 다르게 해석하는지 손으로 하나씩 짚어가면서야 desync 메커니즘이 보였다. 그리고 두 건 다 기존에 이미 올라와 있던 유사 이슈들(minimp3 #122/#134, dr_wav의 CVE-2026-29022)과 안 겹친다는 걸 직접 증명해야 했는데, 이게 코드 읽는 것보다 더 품이 들었다.

배운 점

퍼저는 문 앞까지만 데려다준다. 문을 여는 건 결국 사람의 몫이다.

이 구간을 건너뛰면 리포트는 "크래시가 남" 수준에서 못 벗어난다. 오래됐지만 여전히 여기저기 임베드되어 현역으로 돌아가는 single-file 라이브러리들은 생각보다 미탐지 영역이 넓다는 것도 이번에 확인했다. 그리고 벤더 반응은 전적으로 복불복이라는 것 — 하루 만에 고쳐주는 메인테이너도 있고, 반년째 조용한 메인테이너도 있다. 둘 다 겪어봐야 아는 것들이었다.

마치며

CVE 두 개가 커리어의 전환점씩이나 되는 건 아니지만, 퍼저가 던져준 크래시 하나를 끝까지 손으로 추적해서 "왜"까지 설명할 수 있게 되는 경험은 확실히 남는다. minimp3 건은 패치되는 대로 PoC까지 마저 채워서 다시 올리겠다.

작성자: Alert_K (ANH) 관련 CVE: CVE-2026-37715 · CVE-2026-37716 참고: lieff/minimp3#140 · mackron/dr_libs#300