TECH 으로 돌아가기
TECH HACKER NEWS 9분 읽기 25 READS

AES-256을 내걸고 9비트로 잠근 PDF: 암호화 구현의 난수 함정

AES-256을 내걸고 9비트로 잠근 PDF: 암호화 구현의 난수 함정
SOURCE IMAGE · HACKER NEWS

PDF 문서 암호화는 오랫동안 기업과 공공기관에서 민감 문서를 보호하는 기본 수단으로 쓰여 왔다. 그런데 보안 연구팀 v12가 공개한 분석은 'AES-256으로 암호화했다'는 표시가 실제 256비트 수준의 안전성을 보장하지 않을 수 있음을 보여준다. 여러 PDF 구현이 AES-256을 내세우면서도, 정작 키를 만들어내는 난수가 열거 가능한 아주 좁은 상태 공간이나 안전하지 않은 난수원에서 나오고 있었다는 것이다. 대표적으로 MuPDF에서는 256비트 키에 실제로 영향을 주는 무작위 상태가 단 9비트에 불과했고, Foxit 계열은 시각 기반 난수를 사용해 키 복원이 사실상 자명한 수준이었다.

암호는 문서를 직접 잠그지 않는다

문제를 이해하려면 PDF 암호화 구조를 먼저 짚어야 한다. PDF 암호화는 문자열과 데이터 스트림을 보호하며, 명시적으로 제외하지 않는 한 메타데이터도 포함된다. 암호 방식은 RC4에서 AES로 옮겨 가고 암호 해싱을 무차별 대입에 강하게 만드는 식으로 여러 차례 개정되어, 현재 리비전 6은 AES-256을 쓴다. 핵심은 사용자가 입력한 암호가 문서를 직접 암호화하지 않는다는 점이다. 구현체는 무작위 32바이트 파일 키를 생성해 본문을 암호화하고, 암호와 솔트를 해싱해 얻은 별도의 키로 이 32바이트 파일 키를 감싼다. 리비전 6은 이 해싱을 일부러 느리게 설계했지만, 그 이전인 리비전 5는 그렇지 않다.

PDF에는 열람용 사용자 암호와 권한 변경용 소유자 암호가 따로 있는데, 둘 다 같은 파일 키를 푼다. 즉 제약은 뷰어의 선의에 달려 있을 뿐이어서, 열람 암호만 있어도 제약 없는 사본을 만들 수 있다. 바로 이 32바이트 파일 키를 만드는 난수가 허술하면 암호 없이도 문서가 열린다는 것이 이번 분석의 출발점이다.

상위 비트는 하위 비트를 건드리지 못한다

MuPDF 1.28.5 이전 버전은 drand48 계열 구현으로 난수를 만든다. 이 생성기는 프로그램 실행 중 현재 epoch 초 값으로 단 한 번만 시드된다. 48비트 선형 합동 생성기(LCG)로, 상태는 16비트 워드 세 개로 기록되는데 가장 낮은 워드는 상수 0x330e로 고정되고 32비트 타임스탬프가 상태의 16~47비트를 차지한다. 키가 필요할 때 호출되는 fz_memrnd()는 난수를 뽑고 생성기를 전진시킨 뒤 그 최하위 바이트를 난수 바이트로 쓴다. 이때 시프트가 0~16비트를, unsigned char 변환이 24비트 위를 버리므로, 출력되는 모든 바이트는 상태의 17~24비트에서만 나온다.

LCG의 구조적 특성이 여기서 치명적으로 작용한다. 곱셈과 덧셈은 자리올림으로 비트를 위로 전파할 수 있어도 아래로는 내려보내지 못한다. 출력 바이트가 17~24비트에서만 나오므로 24비트 위의 값은 생성기를 전진시켜도 무시해도 된다. 그렇게 남는 25비트 중 하위 16비트는 항상 0x330e이니, 결국 타임스탬프에서 유효한 것은 9비트뿐이다. 512초만큼 차이 나는 타임스탬프는 완전히 동일한 바이트 스트림을 만든다. 키 공간이 이렇게 좁으면 가능한 32바이트 키 512가지를 모두 생성해 시험 복호화하면 된다. MuPDF는 평문으로 트레일러에 저장되는 문서 /ID에도 같은 생성기를 쓰고, 새 문서를 쓸 때 같은 LCG에서 32바이트를 먼저 뽑은 뒤 바로 복호화 키를 샘플링하므로 /ID를 추가 검증값으로 활용할 수 있다. 최적화하지 않은 순수 파이썬 무차별 대입으로 512개 스트림을 전부 탐색하는 데 약 11밀리초가 걸렸고, 그렇게 찾은 키는 두 암호 중 어느 것도 없이 문서를 풀었다.

같은 패턴이 여러 제품으로 번졌다

Foxit PDF Reader 2026.1.3.36551은 파일 키를 현재 타임스탬프와 32바이트의 미초기화 데이터, 그리고 상수의 해시로 유도한다. 그러나 이 미초기화 바이트는 실제로는 0으로 채워져 아무 무작위성도 더하지 않는다. /Perms 권한 블록에 알려진 평문이 들어 있어, 후보 검증은 해시 한 번과 AES 블록 한 번 복호화로 끝나며 느린 암호 해싱도 문서 복호화도 필요 없다. 연구팀의 Rust 구현은 현재 시각부터 거꾸로 타임스탬프를 훑으며 Rayon으로 병렬 처리했고, Ryzen 9 7950X 32스레드에서 초당 3억 7천만 개 안팎을 지속적으로 처리했다. 1970년부터 현재까지 전 구간을 훑어도 이 속도면 되고, 비교적 최근에 생성된 문서라면 복원은 밀리초 단위다.

이 패턴은 여러 PDF 편집 도구에서 반복적으로 나타났는데, 그 출처를 따라가 보면 2014년 5월 PDFium의 최초 공개 커밋에 이미 존재했고 파일 헤더가 원본 코드를 Foxit으로 명시하고 있다. 다른 편집기들은 PDFium을 통해 이 코드를 받아들였을 가능성이 크다. 해시에 들어가는 32바이트 중간값은 자기 자신에게 해싱되는 미초기화 키 버퍼 m_EncryptKey로, 0으로 채워졌다고 가정하면 이는 Foxit이 지금도 쓰는 키 유도 방식과 정확히 같다. 한편 OpenPDF 3.0.5와 iText 9.7.1은 암호학적 난수를 IVGenerator 클래스로 보내는데, 이는 밀리초 타임스탬프와 JVM의 가용 메모리로 시드된 ARC4 스트림이다. 같은 스트림이 파일 키와 /U·/O에 평문으로 저장되는 32바이트 솔트를 함께 만들어내므로, 타임스탬프와 가용 메모리 추정값을 솔트에 맞춰 보고 인접 바이트에서 키를 복원할 수 있다. 앞선 두 사례보다 변수의 폭은 크지만 결론은 같다.

실무자가 가져갈 교훈

공통점은 명확하다. 세 구현 모두 파일 키를 암호학적으로 안전하지 않은 난수원에서 만들었다. 사용자 암호에서 키를 유도하던 옛 방식의 약점을 보완하려던 업그레이드가, 오히려 실제로는 더 약한 키를 낳은 셈이다. 이는 알고리즘 자체가 아니라 그 알고리즘에 키를 공급하는 난수 설계에서 보안이 무너진 전형적인 사례다. 실무 관점에서 '라이브러리가 AES-256을 지원한다'는 사실만으로 안심해서는 안 되며, 키와 솔트가 운영체제의 CSPRNG에서 나오는지, 혹은 타임스탬프나 미초기화 버퍼 같은 예측 가능한 값에 의존하지는 않는지까지 확인해야 한다는 점을 상기시킨다.

이 문제들은 각 벤더에 제보되었고 MuPDF의 경우 수정되었다. 다만 이미 배포된 과거 문서는 소급해서 보호할 수 없으므로, 취약한 버전으로 암호화된 PDF가 외부에 돌아다니고 있다면 그 문서는 지금도 영향권에 있다. 민감 문서를 과거에 특정 도구로 암호화해 배포한 적이 있다면, 해당 구현과 버전을 점검하고 필요하다면 재암호화나 폐기를 검토할 필요가 있다. 연구팀은 MuPDF와 Foxit에 대한 복호화 도구를 공개했는데, 이는 방어 측에서 자신이 보유한 문서의 노출 여부를 직접 검증하는 데에도 쓸 수 있다.

SOURCE · HACKER NEWS
원문 전체 보기 → https://v12.sh/blog/the-case-of-the-9bit-aes-key
SHARE
NEXT · CHOOSE

변화를 읽었다면,
내가 만들 수익 구조를 고릅니다.

정보를 더 모으는 데서 멈추지 않고, 광고·외주·판매·중개·구독 중 내 상황에 맞는 출발점을 정해보세요.

21가지 수익 구조 살펴보기 →
처리 중...