The case of the 9 bit AES key
9비트 AES 키의 사례
MuPDF, Foxit, OpenPDF, iText의 PDF 암호화 구현에서 AES-256 파일 키를 만드는 난수가 예측 가능해 암호 없이 문서를 복호화할 수 있는 취약점이 발견됐습니다. MuPDF는 가능한 키가 512개뿐이었고, Foxit은 생성 시각을 대입해 키를 찾을 수 있었습니다. MuPDF는 수정됐지만 이미 만들어진 PDF는 영향을 받을 수 있습니다.
- 주제
에디터 노트
이번 사례에서 직접 드러난 문제는 AES-256 자체가 아니라 파일 키를 만드는 난수의 예측 가능성입니다. MuPDF에서는 출력에 영향을 주는 시각 정보가 9비트로 줄었고 Foxit에서는 알려진 평문으로 시각 후보를 빠르게 검사했습니다. 긴 키 길이가 취약한 생성기를 보완하지 못한다는 설명은 구현 세부와 검색 결과가 뒷받침합니다. 댓글에서는 암호학용 시스템 난수 사용과 PDF 파서의 메모리 안전성이 함께 거론됩니다. 둘은 별개의 위험입니다. 이 글은 키 복구를 다루며 C 기반 파서의 메모리 취약점을 입증하지 않습니다. PDF 생태계의 복잡성과 구현별 암호화 난수 문제를 구분해 읽을 필요가 있습니다. 이미 만들어진 파일은 MuPDF 수정만으로 보호되지 않는다는 점도 명시돼 있습니다.
AI 요약
PDF 암호화는 문서 내용을 임의의 32바이트 파일 키로 암호화하고, 사용자 또는 소유자 암호와 salt를 해시해 파일 키를 감쌉니다. 따라서 AES-256을 사용하더라도 파일 키를 만들 때 난수가 예측 가능하면 암호를 모른 채 파일 키를 찾을 수 있습니다. 글은 MuPDF, Foxit, OpenPDF, iText에서 이런 문제가 확인됐다고 설명합니다.
MuPDF
MuPDF 1.28.5 이전 버전은 48비트 선형 합동 생성기인 drand48 계열 난수 생성기를 사용했습니다. 생성기는 현재 시각을 초 단위로 받아 초기화하고, 키를 만들 때마다 상태를 갱신한 뒤 난수의 하위 8비트를 출력합니다. 이 과정에서는 상태의 17~24번째 비트만 출력에 영향을 줍니다. 그보다 높은 비트는 이후 상태 갱신을 거쳐도 낮은 비트에 영향을 주지 않습니다. 상태 하위 16비트는 상수 0x330e이므로, 키 출력에 영향을 주는 시각의 비트는 9개뿐입니다. 따라서 가능한 바이트열은 512개이며, 각각의 32바이트 키를 만들어 시험하면 됩니다. 512개를 순회하는 순수 Python 구현은 약 11밀리초가 걸렸습니다. MuPDF는 문서의 평문 trailer에 저장되는 /ID도 같은 생성기로 만들며, /ID와 파일 키 사이의 연속된 출력 관계를 확인에 활용할 수 있습니다. 해당 문제는 1.28.5에서 수정됐습니다.
Foxit
Foxit PDF Reader 2026.1.3.36551은 현재 시각과 32바이트 메모리 값, 상수를 해시해 파일 키를 만듭니다. 글에 따르면 해당 메모리 값은 실제로 0으로 채워져 추가 난수를 제공하지 않습니다. PDF의 /Perms 블록에는 알려진 평문이 있어 후보 시각마다 해시 한 번과 AES 블록 복호화 한 번으로 키를 검사할 수 있습니다. 암호 해시 계산이나 문서 전체 복호화는 필요하지 않습니다. 글의 Rust 구현은 Ryzen 9 7950X 32스레드에서 초당 3억 7천만~3억 7천6백만 개의 시각을 검사했습니다. 10년치 시각은 0.84초, 1970년부터 현재까지는 4.85초에 검색했습니다. 해당 키 도출 방식은 PDFium의 2014년 첫 공개 커밋에도 있었고, 코드 헤더에는 Foxit이 원본으로 표시돼 있습니다.
OpenPDF와 iText
OpenPDF 3.0.5와 iText 9.7.1은 밀리초 단위 시각과 JVM의 여유 메모리를 seed로 삼는 ARC4 스트림을 통해 암호학적 난수를 생성합니다. 같은 스트림에서 파일 키와 평문으로 저장되는 /U, /O의 32바이트 salt를 만듭니다. salt를 이용해 시각과 여유 메모리 후보를 검사한 뒤 인접한 스트림 출력에서 파일 키를 복구할 수 있습니다. 이 경우 MuPDF나 Foxit보다 후보 변수가 많지만, 글은 AES-256의 키 길이가 난수 생성기의 실제 상태 공간을 보장하지 않는다고 지적합니다.
Hacker News 반응
- @paulpauper — 약한 하드웨어 지갑 엔트로피 취약점과 비슷합니다.
- @ubutler — 자체 난수 생성기를 만들 때 첫 번째 규칙은 만들지 않는 것입니다. 두 번째 규칙은 제발 만들지 않는 것입니다.
- @j16sdiz — 그래서 시스템 난수 생성기를 씁니다.
- @cowlevel — rand(3)이 아니라 암호학용 난수 생성기를 써야 합니다. 시스템 관리자라면 VM이나 임베디드 장치에서 키를 만들 때 특히 조심해야 합니다. 엔트로피 원천을 실제보다 좋게 여길 수 있고 VM 복제본마다 같은 키가 만들어질 수도 있습니다. ECDSA에서 같은 키와 nonce로 서로 다른 메시지에 서명하면 개인 키가 즉시 노출됩니다.
- @tptacek — MuPDF가 drand48 계열을 썼다는 대목을 보면, 제대로 된 암호화를 하지 않은 셈입니다. 키 공간의 비트 수가 실제 문제의 전부는 아닙니다. 키가 암호학적으로 무작위가 아니었습니다.
- @jeffbee — C로 작성한 PDF 도구를 쓰려면 정신이 나가 있어야 합니다.
- @recursive — 사용자가 만든 데이터와 복잡한 파일 형식을 처리하는 코드에서 버퍼 오버플로가 발생할 수 있습니다.
- @tptacek — 그 주장은 과장이라고 봅니다. 메모리 안전하지 않은 PDF 구현이 대부분이지만 PDF는 가능한 거의 모든 표시 형식과 렌더러를 끌어안은 거대한 메타 형식입니다. 렌더러 중에는 관리가 부실하고 오래된 참조 구현도 많습니다. PDF는 다루기 까다롭습니다.
- @zamalek — 파일 형식은 버퍼 취약점이 생기기 쉬운 지점입니다. 영상과 음성 코덱도 손으로 최적화한 프레임 디코딩 어셈블리보다 메타데이터와 인코딩된 프레임을 메모리로 읽는 부분에서 취약점이 자주 나옵니다. Rust도 바이너리 데이터를 다루는 crate가 있을 정도입니다. Rust만 써야 한다는 뜻은 아니며, 메모리 안전 언어라면 되고 Rust는 성능도 경쟁력이 있습니다.
- @tialaramex — 이런 작업에는 WUFFS가 적절합니다. Rust는 범용 언어지만 파일 형식을 안전하게 다루는 문제는 이미 반복해서 겪었습니다. 파일 형식 처리에 특화된 언어를 써야 합니다.
- @philsnow — PDF 프로그램이 자체 암호화를 구현하는 건 좋지 않습니다. 암호화 설계뿐 아니라 사용 경험도 잘 만들어야 하는데 PDF 도구 개발자가 그 분야에 능숙하리란 보장은 없습니다. 민감한 파일이 PDF만 있는 것도 아닌데 왜 파일 형식마다 각자 암호화를 넣어야 하는지 모르겠습니다.
원문: v12.sh / 번역·요약: Trawling