Technical Article

Delphi에서 PDFium을 사용한 PDF 보안 위험 감사

PDF는 단순히 종이 문서가 아닙니다. 파일이 열릴 때 실행되는 스크립트, 외부 프로그램을 여는 링크, 웹 서버와 통신하는 링크, 파일 내부에 중첩된 파일, 그리고 작성자가 서명한 후 변경되지 않았음을 증명하는 서명 정보 등을 내장할 수 있는 지능형 컨테이너입니다. 신뢰하지 못하는 출처로부터 파일을 수신한 경우 가장 안전한 사전 조치는 문서를 바로 렌더링하지 않는 것입니다. 문서 자체 메타 데이터를 먼저 검사하여 해당 파일이 수행할 수 있는 모든 동작의 인벤토리를 구축함으로써, 담당자가 보안 워크플로에 진입시켜도 안전한 문서인지 먼저 판단하게 만들어야 합니다.

이 글에서는 Delphi 및 Lazarus용 PDFium 컴포넌트를 사용하여 이러한 보안 위협 요소를 정적으로 진단하는 읽기 전용 감사(audit) 프로세스를 보여줍니다. 감사는 페이지 렌더링 작업을 전혀 수행하지 않습니다. 단지 문서 구조를 분석하고 작동 관련 요소를 나열하여 일반 텍스트 리포트를 작성할 뿐입니다. 이는 외부인을 문 앞에서 안전하게 확인하는 절차와 같으며, 단순히 겉모양만 보고 신뢰하는 것과는 분명한 차이가 있습니다.

감사의 정의와 한계

해당 감사의 수행 범위를 명확히 규정해 둘 필요가 있습니다. 샌드박스 프리뷰는 엄격한 보안 통제하에 임시 렌더링을 실행하여 로컬 시스템에 무해한 상태로 문서를 모니터링하게 지원합니다. 감사는 이보다 앞선 단계에서 작동합니다. 렌더링 없이 순수 문서 위협 명세만을 진단합니다: 즉 활성 스크립트 수, 링크 액션 종류, 서명 유무 및 신뢰 수준, 첨부 파일 목록 등을 리포팅합니다. 이메일 수신, 업로드 양식 또는 파트너 네트워크를 통해 문서가 유입되어 신뢰 경계(trust boundary)를 지날 때, 실제로 문서를 열기 전에 선행되어야 하는 초기 게이트웨이 역할을 합니다.

컴포넌트가 문서를 로드하는 동일한 방식으로 로딩을 실행합니다. 파일 이름을 할당하고 활성화하면 단일 페이지도 렌더링하지 않고 크로스 레퍼런스(cross-reference) 및 카탈로그 데이터를 파싱합니다. 이하 단계의 모든 연산은 이 렌더링되지 않은 상태에서 수행됩니다.

var
  Pdf: TPdf;
begin
  Pdf := TPdf.Create(nil);
  try
    Pdf.FileName := 'Incoming_Invoice.pdf';
    Pdf.Active := True;          // parses structure, renders nothing
    // audit the loaded document here
  finally
    Pdf.Free;
  end;
end;

이름 트리의 문서 JavaScript

가장 먼저 파악해야 할 요소는 실행 코드입니다. PDF는 문서 레벨 JavaScript를 포함할 수 있습니다. 이는 특정 페이지나 양식 필드에 종속되지 않고 문서 자체에 바인딩되어 있으며, /Names 트리의 /JavaScript 항목에 보관됩니다. 규격 준수 뷰어들은 문서 오프닝 시 이 스크립트들을 자동 실행합니다. 이는 다수의 PDF 기반 공격 도구들이 채택하는 구조로, 사용자가 문서를 더블 클릭하는 즉시 어떠한 본문 확인도 거치기 전에 시스템 명령을 먼저 구동하게 만듭니다.

감사 모듈은 각 스크립트에 대해 두 가지 사항을 확인해야 합니다: 스크립트의 존재 여부와 내용입니다. 컴포넌트는 식별된 스크립트 수와 스크립트 이름 및 전체 코드가 담긴 레코드 정보를 노출합니다. 소스 코드를 직접 확인하는 과정이 대단히 중요합니다. Doc.0처럼 단순 명명된 스크립트더라도 내부에서 app.launchURL 같은 위험 함수를 은밀하게 호출하거나 악성 문자열을 조립해 외부로 전달할 수 있기 때문입니다. 소스 코드를 추출하여 보안 담당자가 직접 확인하게 하는 것이 문서 오픈 시 자동 구동되는 위협 요소를 격리하는 핵심입니다.

var
  I: Integer;
  Action: TPdfJavaScriptAction;
begin
  if Pdf.JavaScriptActionCount > 0 then
    WriteLn('WARNING: document runs ', Pdf.JavaScriptActionCount,
            ' script(s) on open');
  for I := 0 to Pdf.JavaScriptActionCount - 1 do
  begin
    Action := Pdf.JavaScriptAction[I];
    WriteLn('  script "', Action.Name, '":');
    WriteLn(Action.Script);   // full body, for a human to read
  end;
end;

문서 스크립트가 없다고 해서 완전히 무해하다고 판단할 수는 없습니다. 특정 페이지나 양식 필드 내부에도 스크립트가 존재할 수 있기 때문입니다. 하지만 문서 스크립트가 발견된 경우에는 반드시 정밀 분석을 실시해야 합니다. 스크립트 탐지 수 자체가 유용한 안전장치 역할을 하며, 추출된 스크립트 소스는 문서의 위해 여부를 평가하는 근거가 됩니다.

Launch 및 URI 액션

그다음 진단 대상은 링크 및 주석에 정의된 액션입니다. 감사 관점에서 가장 중요한 액션은 두 가지입니다. Launch 액션은 링크 트리거 시 외부 실행 파일을 구동하거나 로컬 문서를 엽니다. URI 액션은 외부 웹 주소로 브라우저를 연결합니다. 보안 리뷰어는 실제로 링크를 클릭하지 않고도, 3페이지에 위치한 버튼에 cmd.exe를 실행하게 지시하거나 공식 사이트 주소와 다르게 위장된 악성 도메인이 바인딩되어 있는지를 식별할 수 있어야 합니다.

컴포넌트는 탐색된 링크의 형식을 식별하고 각 링크의 액션 정보와 대상 경로를 노출하므로, 감사 프로세스를 통해 탐지된 모든 Launch 및 URI 액션 대상을 목록화할 수 있습니다. 이는 링크를 실행하지 않고 정보만 취합하는 리포팅 과정입니다. 감사 루틴은 문서 구조에서 주소 값을 파싱해 리포트에 기입할 뿐 결코 링크를 추적하지 않습니다.

실제로 링크 액션의 실행 여부는 문서를 화면에 렌더링하는 뷰어 컨트롤 레벨에서 통제하며, 기본 보안 정책은 매우 방어적입니다. TPdfView 컨트롤은 클릭 시 자동으로 실행할 링크의 종류를 정의하는 LinkOptions 집합 속성을 제공합니다. 기본값은 [loAutoGoto, loAutoOpenURI]로 설정되어 문서 내부 이동 및 일반 웹 링크는 허용하지만, loAutoLaunch 속성이 누락되어 있으므로 시프트 및 외부 프로그램 호출 명령은 기본적으로 자동 차단됩니다. 보안 진단 워크플로에서는 한 걸음 더 나아가 이 옵션 세트를 완전히 비워둠으로써 문서가 안전한지 분석하는 동안 어떠한 링크 동작도 발생하지 않도록 완전 무력화할 수 있습니다.

// Audit posture for the viewer: nothing auto-runs, nothing auto-opens.
View.LinkOptions := [];

// The shipped default already withholds launch:
//   default = [loAutoGoto, loAutoOpenURI]
//   loAutoLaunch is NOT in the default set, so external programs
//   are never started on a stray click out of the box.

Launch 액션을 기본 정책에서 차단하는 이유는 명확합니다. 문서 내부의 이동은 무해하며 웹 연결은 화면에 주소가 식별되고 임의 차단이 가능하지만, 클릭 한 번으로 외부 실행 파일을 시스템 권한으로 실행하는 것은 보안에 치명적인 구멍이 될 수 있으므로 수동으로 허용하지 않는 한 차단됩니다. 감사 루틴은 단순 링크 조사만이 목적이므로 일반적인 내부 이동 링크 동작마저 비활성화하여 통제합니다.

디지털 서명 MDP 권한 수준

디지털 서명은 다른 차원의 문제입니다. 일반 디지털 서명은 서명 완료 시점의 문서 바이트 변형 여부만을 검증합니다. 한 단계 더 나아가 MDP(Modification Detection and Prevention) 규칙과 함께 체결되는 인증 서명(certification signature)은 서명 완료 후 어느 영역까지 수정 가능한지를 선언적으로 규정하며, 뷰어는 허용 범위를 넘는 임의 변경을 감지하여 사용자에게 알립니다. 이 권한 수준을 해독하면 문서의 서명 인증 수준과 문서 구조의 통제 범위를 파악할 수 있습니다.

MDP 권한은 정수 코드로 세 수준으로 표현됩니다. 수준 1은 어떠한 추가 수정도 절대 불허함을 의미하며, 사소한 레이아웃 수정조차 서명 인증을 깨지게 만듭니다. 수준 2는 계약서 등에 흔히 쓰이는 정책으로, 다른 수치는 손대지 않은 채 입력 영역 데이터 작성 및 서명 필드 서명 행위만을 허용합니다. 수준 3은 데이터 양식 작성 및 서명 필드 서명에 더해 자유로운 주석(annotation) 편집 권한을 연계합니다. 이 등급을 조회하면 문서 작성 정책의 위반 사항을 식별할 수 있습니다: 예를 들어 수준 1로 서명된 문서인데 데이터 입력용 스크립트나 작성 폼 위젯이 버젓이 연동되고 있다면 이는 앞뒤가 맞지 않는 구조로 감지 대상이 됩니다.

컴포넌트는 파일에 적용된 총서명 수와 내부 FPDFSignatureObj_GetDocMDPPermission API 호출로부터 매핑된 각 서명의 MDP 권한 속성을 포함하는 레코드 구조를 리포팅합니다. 만약 권한 값이 0이면 인증형 서명이 아닌 단순 서명 방식으로 체결된 것이므로 문서 레벨의 제한 사항은 유효하지 않습니다.

var
  I: Integer;
  Sig: TPdfSignature;
begin
  if Pdf.SignatureCount = 0 then
    WriteLn('document is not signed')
  else
    for I := 0 to Pdf.SignatureCount - 1 do
    begin
      Sig := Pdf.Signature[I];
      case Sig.Permission of
        1: WriteLn('certified: no changes allowed');
        2: WriteLn('certified: form fill and signing allowed');
        3: WriteLn('certified: form fill, signing and annotations allowed');
      else
        WriteLn('signed, but not a DocMDP certification');
      end;
    end;
end;

해당 감사 단계는 서명의 수학적인 무결성이나 인증 기관(CA) 체인 신뢰성 검증을 전담하지는 않습니다. 대신 서명을 통해 강제하고자 하는 정책 속성: 즉 해당 등급으로 작성 잠금이 설정되어 있음을 리포팅합니다. 보안 리뷰어는 이 정보를 활용하여 서명 완료 후 유입된 개별 가상 스크립트나 임의 구조 변경이 저자의 원래 배포 및 보안 봉인 규칙에 위배되는지 판별할 수 있습니다.

나머지 분석 영역: 첨부 파일 및 XFA

안전 진단을 위해 확인할 두 가지 요소가 더 있습니다. 임베디드 첨부 파일(embedded files)은 PDF 내에 포함되어 있는 별도 파일들입니다. 겉으로는 정상적인 보고서 파일로 보이지만 첨부 파일 트리에 악성 실행 파일이나 2차 위협 PDF를 은폐하여 전송하는 전통적인 우회 방식이 활용되기도 합니다. 컴포넌트는 첨부된 파일의 총 수와 파일명을 노출하므로, 실제로 첨부 파일을 추출하거나 실행하지 않고 목록 정보만을 감사 리포트에 수집하여 무력화할 수 있습니다.

XFA의 통합 여부 역시 모니터링 대상입니다. XFA 기반 양식은 일반적인 정적 AcroForm 대신 독자적인 드로잉과 스크립트 동작 방식을 기반으로 설계된 복잡한 XML 폼 레이아웃 규칙을 따릅니다. XFA를 일일이 해체하지 않더라도 단순히 해당 구조가 내장되어 있음을 식별하는 것만으로 충분합니다. XFA가 식별되었다는 것은 문서가 고급 사용자 인터랙션을 동반하고 있어 위협에 노출될 기하학적인 영역이 넓음을 알리는 것이기 때문입니다. 컴포넌트는 이를 부울(boolean) 속성으로 알립니다.

var
  I: Integer;
begin
  if Pdf.XFA then
    WriteLn('NOTE: document contains an XFA form layer');

  if Pdf.AttachmentCount > 0 then
  begin
    WriteLn('embedded files: ', Pdf.AttachmentCount);
    for I := 0 to Pdf.AttachmentCount - 1 do
      WriteLn('  - ', Pdf.AttachmentName[I]);
  end;
end;

보고서를 작성하는 하나의 읽기 전용 루틴

모든 요소를 정리하면 문서 검사 루틴은 다음과 같은 단일 프로시저로 완성됩니다: 문서를 로딩하고, 포함된 스크립트 수와 소스를 나열하며, 연동된 Launch/URI 링크 대상을 조사하고, 서명 권한 등급을 보고하고, 첨부 파일과 XFA를 식별하여 최종 텍스트 로그에 기입하는 것입니다. 드로잉 및 페이지 로드가 수행되지 않으므로 연산 속도가 매우 빠르며 악의적인 본문 콘텐츠가 로드되어 브라우저 오류를 발생시키지도 못합니다. 생성되는 텍스트 파일은 리뷰어가 수동 진단하거나 자동 처리 시스템의 조건문 게이트웨이에 활용하기 적합한 단일 명세 기록입니다.

실무에 적합한 구조는 진단 결과를 행 단위로 취합하되 위험도가 높은 경고 항목에 식별 문구(prefix)를 부여해 상위에 노출하고 결과 로그를 분석 문서 경로에 함께 관리하는 방식입니다. 스크립트나 위협 링크, 첨부 파일 및 XFA를 포함하지 않고 깨끗한 일반 서명 상태인 문서는 게이트웨이를 바로 통과시킵니다. 여러 위협 플래그가 한 번에 탐지된 문서들은 리뷰어가 직접 사후 진단을 내리기 전까지 프로세스를 대기 상태로 통제합니다. 감사가 알아서 임의 판단을 내리는 것은 아닙니다. 감사는 의사 결정자가 맹목적인 신뢰 대신 명확한 명세 정보를 기반으로 판단을 내릴 수 있도록 정보를 수집하는 역할을 담당합니다.

감사를 안전하게 통과한 문서를 실제로 열어 확인해야 하는 경우에도 일반 기본 뷰어 프로그램 대신 제어된 제한 모드로 열어야 합니다. Our walkthrough on building a secure PDF preview in Delphi에서 설명하는 보안 설정을 적용하면 링크 실행 연동이나 내부 액션의 발현을 무력화한 상태에서 문서를 안전하게 미리 볼 수 있습니다. 수집된 위협 명세 분석기를 종합 검토 워크플로 파이프라인에 이식하는 방식은 PDF 반입 및 위협 진단 워크벤치 문서를 참조하십시오. 두 주제 모두 렌더링 무력화 읽기 방식을 기반으로 구현되었으며, 렌더링, 텍스트, 폼 양식, 디지털 서명 API와 함께 Delphi 및 C++Builder용 PDFium 컴포넌트로 제공됩니다.