Technical Article

메모리 안전 PDF 파싱: 악성 문서에 대한 방어

PDF 문서는 놀라울 정도로 강력하지만 그 힘에는 고유한 보안 위험이 따릅니다. PDF는 포함된 파일, 대화형 JavaScript 및 복잡한 바이너리 스트림을 지원하기 때문에 멀웨어 전송 벡터로 자주 사용됩니다. 잘못 작성된 PDF 파서의 버퍼 오버플로, 경계 이탈 읽기(out-of-bounds reads) 및 정수 오버플로는 원격 코드 실행(RCE)으로 이어질 수 있습니다.

사용자가 업로드한 PDF를 허용하는 애플리케이션(예: 문서 수집 포털)을 Delphi에서 구축하는 경우, 메모리가 안전한 PDF 파싱을 보장하는 것은 중요한 보안 요구 사항입니다.

일반적인 PDF 공격 벡터

악성 PDF는 일반적으로 운영 체제보다 파서 자체의 취약점을 표적으로 삼습니다. 일반적인 기술은 다음과 같습니다.

  • 형식이 잘못된 상호 참조(XRef) 테이블: 범위를 벗어나는 포인터 오프셋을 조작하여 파서를 충돌시키거나 메모리를 노출시킵니다.
  • 무한 루프: PDF 객체 간에 순환 참조(예: 객체 A가 객체 B를 참조하고 다시 객체 B가 객체 A를 참조)를 생성하여 스택을 고갈시킵니다.
  • 폭발적인 압축 해제(Zip 폭탄): 몇 킬로바이트에서 기가바이트로 압축이 풀려 시스템 메모리를 고갈시키는 FlateDecode 스트림.

Delphi의 방어적 파싱 전략

Delphi에서 기본적으로 PDF를 구문 분석할 때는 방어적으로 프로그래밍해야 합니다. PDF 딕셔너리에 제공된 메타데이터를 신뢰해서는 안 됩니다.

1. 순환 참조 끊기

PDF 객체 트리를 재귀적으로 순회할 때 무한 루프를 방지하려면 방문한 객체의 기록을 유지해야 합니다.

uses
  System.Generics.Collections, System.SysUtils;

// A safe recursive function to walk the PDF tree
procedure ParsePDFDictionary(DictObj: TPDFDictionary; Visited: TList<Integer>);
var
  ObjID: Integer;
begin
  ObjID := DictObj.ObjectID;
  
  if Visited.Contains(ObjID) then
  begin
    Writeln('Warning: Circular reference detected. Aborting branch.');
    Exit;
  end;
  
  Visited.Add(ObjID);
  
  try
    // Process child objects safely...
  finally
    // Allow siblings to traverse, but prevent vertical recursion loops
    Visited.Remove(ObjID);
  end;
end;

2. Zip 폭탄으로부터 보호

FlateDecode 필터를 적용하여 스트림의 압축을 풀 때 최대 확장 크기를 엄격하게 제한해야 합니다. /Length 딕셔너리 키를 기반으로 맹목적으로 메모리를 할당하지 마십시오.

const
  MAX_DECOMPRESSED_SIZE = 1024 * 1024 * 50; // 50 MB safety limit

procedure DecompressPDFStream(CompressedStream, OutputTarget: TStream);
var
  ZLibStream: TZDecompressionStream;
  Buffer: array[0..8191] of Byte;
  BytesRead, TotalRead: Integer;
begin
  ZLibStream := TZDecompressionStream.Create(CompressedStream);
  try
    TotalRead := 0;
    repeat
      BytesRead := ZLibStream.Read(Buffer[0], SizeOf(Buffer));
      if BytesRead > 0 then
      begin
        TotalRead := TotalRead + BytesRead;
        if TotalRead > MAX_DECOMPRESSED_SIZE then
          raise Exception.Create('Security Exception: Decompression bomb detected!');
          
        OutputTarget.WriteBuffer(Buffer[0], BytesRead);
      end;
    until BytesRead = 0;
  finally
    ZLibStream.Free;
  end;
end;

강화된 엔진 및 안전한 구성 요소 활용

처음부터 완전히 안전한 PDF 파서를 작성하는 것은 엄청난 작업입니다. 업계 표준 접근 방식은 PDFium과 같이 강화되고 고강도의 퍼징 테스트(fuzz-tested)를 거친 엔진을 사용하거나, 엄격하게 테스트된 네이티브 라이브러리에 의존하는 것입니다.

PDFium은 Google Chrome에서 사용하는 핵심 렌더링 엔진입니다. Chrome은 매일 수백만 개의 신뢰할 수 없는 PDF를 처리하기 때문에, PDFium은 Google의 Project Zero에 의해 공격적이고 지속적인 퍼징을 받습니다. 잘못된 형식의 XRef, 손상된 스트림, 순환 참조를 원활하게 처리합니다.

마찬가지로 HotPDF ComponentDelphi PDF Library와 같은 네이티브 구성 요소는 기본적으로 강력한 방어 파싱 전략을 통합합니다. 이들은 Delphi 및 C++Builder 환경을 위해 특별히 설계된 엄격한 경계 검사, 재귀적 깊이 제한기 및 메모리 누수 방지 메커니즘을 구현합니다.

렌더링을 위해 Delphi 래퍼를 통해 PDFium을 사용하든, 문서 생성 및 처리를 위해 HotPDF와 같은 네이티브 구성 요소를 활용하든, 방어 파서를 직접 작성할 필요 없이 악성 페이로드로부터 사용자 및 서버를 보호하는 엔터프라이즈급 보안 경계를 상속받게 됩니다.

참고: 안전하고 퍼징 테스트를 거친 파싱 기능은 HotPDF Component, Delphi PDF LibraryPDFium Component를 포함한 당사의 전체 제품군에서 사용할 수 있습니다.