- 오래된 single-file 오디오 디코더 두 개(minimp3, dr_libs/dr_wav)를 libFuzzer + AddressSanitizer + UBSan으로 붙여서 크래시를 두 개 건졌다.
- 크래시 로그만 보고 끝내지 않고 코드를 직접 따라가서 두 건 다 근본 원인까지 손으로 추적했다 — 하나는 two-pass 파서 desync로 인한 힙 오버플로우, 하나는 검증 안 된 필드가 10TB 할당으로 이어지는 정수 오버플로우.
- CVE-2026-37715·CVE-2026-37716 두 개를 받았다. dr_wav는 4월에 마스터 브랜치에 패치됐고, minimp3는 반년이 지난 지금도 메인테이너 응답이 없다.
시작하며
"오래된 헤더온리 라이브러리들은 새 퍼저를 돌려도 결과가 안 나오는 영역이 많지 않을까"라는 생각에서 시작했다. 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바이트를 읽는다는 것이었다.
pWav->onRead(pWav->pUserData, fmtData, sizeof(fmtData)); // 항상 16바이트 읽음
sizeInBytes == 0인 fmt 청크를 주면 1차 패스와 2차
패스가 같은 바이트를 서로 다르게 해석하게 된다.
두 패스가 같은 입력을 다르게 해석하면서 어긋나는(desync) 게 이 버그의 본질이다.
디버그 빌드에서는 drwav__metadata_get_memory
(dr_wav.h:2130)에서 assertion이 걸려서 바로 티가
나지만, 릴리스 빌드는 조용히 그냥 overflow가 나버린다. 그리고
overflow되는 크기와 그 자리에 써지는 내용 둘 다 공격자가 제어할 수
있다.
PoC (55바이트) — sizeInBytes=0 하나로 트리거된다.
RIFF 2500 0000 57415645 # RIFF + size=37 + WAVE
666d7420 # "fmt "
00000000 # sizeInBytes = 0 ← 트리거
0000010044ac000044ac0000
01000800
6461746101000000 # "data" chunk
0000000000000080
재현은 drwav_init_memory_with_metadata 하나로 끝난다.
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를 읽는 줄이 있다.
*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이 정확히 이 줄을 짚어줬다:
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
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까지 마저 채워서 다시 올리겠다.