Headline
CVE-2021-25786: Heap-use-after-free in `Pl_ASCII85Decoder::write` · Issue #492 · qpdf/qpdf
An issue was discovered in QPDF version 10.0.4, allows remote attackers to execute arbitrary code via crafted .pdf file to Pl_ASCII85Decoder::write parameter in libqpdf.
- version: 10.0.4
- commit: 78b9d6b
- How to reproduce: qpdf_afl_fuzzer ./poc
The log from ASAN:
==28751==ERROR: AddressSanitizer: heap-use-after-free on address 0x6070000076c5 at pc 0x0000008799bc bp 0x7fffffff6a30 sp 0x7fffffff6a28
WRITE of size 1 at 0x6070000076c5 thread T0
#0 0x8799bb in Pl_ASCII85Decoder::write(unsigned char*, unsigned long) /src/qpdf/libqpdf/Pl_ASCII85Decoder.cc:85:32
#1 0x877725 in Pl_AES_PDF::flush(bool) /src/qpdf/libqpdf/Pl_AES_PDF.cc:241:16
#2 0x877c75 in Pl_AES_PDF::finish() /src/qpdf/libqpdf/Pl_AES_PDF.cc
#3 0x5996ab in QPDF::pipeStreamData(PointerHolder<QPDF::EncryptionParameters>, PointerHolder<InputSource>, QPDF&, int, int, long long, unsigned long, QPDFObjectHandle, bool, Pipeline*, bool, bool) /src/qpdf/libqpdf/QPDF.cc:2899:23
#4 0x599c9d in QPDF::pipeStreamData(int, int, long long, unsigned long, QPDFObjectHandle, Pipeline*, bool, bool) /src/qpdf/libqpdf/QPDF.cc:2920:12
#5 0x77e4c7 in QPDF::Pipe::pipeStreamData(QPDF*, int, int, long long, unsigned long, QPDFObjectHandle, Pipeline*, bool, bool) /src/qpdf/include/qpdf/QPDF.hh:718:19
#6 0x76e02e in QPDF_Stream::pipeStreamData(Pipeline*, bool*, int, qpdf_stream_decode_level_e, bool, bool) /src/qpdf/libqpdf/QPDF_Stream.cc:700:8
#7 0x637e27 in QPDFObjectHandle::pipeStreamData(Pipeline*, int, qpdf_stream_decode_level_e, bool, bool) /src/qpdf/libqpdf/QPDFObjectHandle.cc:1228:51
#8 0x700fbf in QPDFWriter::unparseObject(QPDFObjectHandle, int, int, unsigned long, bool) /src/qpdf/libqpdf/QPDFWriter.cc:1826:24
#9 0x71f286 in QPDFWriter::writeObject(QPDFObjectHandle, int) /src/qpdf/libqpdf/QPDFWriter.cc:2169:2
#10 0x737528 in QPDFWriter::writeStandard() /src/qpdf/libqpdf/QPDFWriter.cc:3676:2
#11 0x72aae7 in QPDFWriter::write() /src/qpdf/libqpdf/QPDFWriter.cc:2745:2
#12 0x51bb07 in FuzzHelper::doWrite(PointerHolder<QPDFWriter>) /src/qpdf_afl_fuzzer.cc:68:12
#13 0x51cb03 in FuzzHelper::testWrite() /src/qpdf_afl_fuzzer.cc:92:5
#14 0x5219a6 in FuzzHelper::doChecks() /src/qpdf_afl_fuzzer.cc:194:5
#15 0x5219a6 in FuzzHelper::run() /src/qpdf_afl_fuzzer.cc:210
#16 0x5221b7 in main /src/qpdf_afl_fuzzer.cc:228:7
#17 0x7ffff6a99bf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6)
#18 0x41eb89 in _start (/src/qpdf_afl_fuzzer+0x41eb89)
0x6070000076c5 is located 21 bytes inside of 78-byte region [0x6070000076b0,0x6070000076fe)
freed by thread T0 here:
#0 0x517d68 in operator delete(void*) (/src/qpdf_afl_fuzzer+0x517d68)
#1 0x7ffff7af21a9 in std::runtime_error::~runtime_error() (/usr/lib/x86_64-linux-gnu/libstdc++.so.6+0xa81a9)
#2 0x599c9d in QPDF::pipeStreamData(int, int, long long, unsigned long, QPDFObjectHandle, Pipeline*, bool, bool) /src/qpdf/libqpdf/QPDF.cc:2920:12
#3 0x77e4c7 in QPDF::Pipe::pipeStreamData(QPDF*, int, int, long long, unsigned long, QPDFObjectHandle, Pipeline*, bool, bool) /src/qpdf/include/qpdf/QPDF.hh:718:19
#4 0x76e02e in QPDF_Stream::pipeStreamData(Pipeline*, bool*, int, qpdf_stream_decode_level_e, bool, bool) /src/qpdf/libqpdf/QPDF_Stream.cc:700:8
#5 0x637e27 in QPDFObjectHandle::pipeStreamData(Pipeline*, int, qpdf_stream_decode_level_e, bool, bool) /src/qpdf/libqpdf/QPDFObjectHandle.cc:1228:51
#6 0x700fbf in QPDFWriter::unparseObject(QPDFObjectHandle, int, int, unsigned long, bool) /src/qpdf/libqpdf/QPDFWriter.cc:1826:24
#7 0x71f286 in QPDFWriter::writeObject(QPDFObjectHandle, int) /src/qpdf/libqpdf/QPDFWriter.cc:2169:2
#8 0x737528 in QPDFWriter::writeStandard() /src/qpdf/libqpdf/QPDFWriter.cc:3676:2
#9 0x72aae7 in QPDFWriter::write() /src/qpdf/libqpdf/QPDFWriter.cc:2745:2
#10 0x51bb07 in FuzzHelper::doWrite(PointerHolder<QPDFWriter>) /src/qpdf_afl_fuzzer.cc:68:12
#11 0x5219a6 in FuzzHelper::doChecks() /src/qpdf_afl_fuzzer.cc:194:5
#12 0x5219a6 in FuzzHelper::run() /src/qpdf_afl_fuzzer.cc:210
#13 0x7ffff6a99bf6 in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x21bf6)
previously allocated by thread T0 here:
#0 0x516ff0 in operator new(unsigned long) (/src/qpdf_afl_fuzzer+0x516ff0)
#1 0x7ffff7b1dc28 in std::string::_Rep::_S_create(unsigned long, unsigned long, std::allocator<char> const&) (/usr/lib/x86_64-linux-gnu/libstdc++.so.6+0xd3c28)
#2 0x883ce8 in Pl_Flate::handleData(unsigned char*, unsigned long, int) /src/qpdf/libqpdf/Pl_Flate.cc:197:12
#3 0x8833d5 in Pl_Flate::write(unsigned char*, unsigned long) /src/qpdf/libqpdf/Pl_Flate.cc:92:9
#4 0x87a0a7 in Pl_ASCII85Decoder::flush() /src/qpdf/libqpdf/Pl_ASCII85Decoder.cc:122:16
#5 0x87997b in Pl_ASCII85Decoder::write(unsigned char*, unsigned long) /src/qpdf/libqpdf/Pl_ASCII85Decoder.cc:88:4
#6 0x877725 in Pl_AES_PDF::flush(bool) /src/qpdf/libqpdf/Pl_AES_PDF.cc:241:16
SUMMARY: AddressSanitizer: heap-use-after-free /src/qpdf/libqpdf/Pl_ASCII85Decoder.cc:85:32 in Pl_ASCII85Decoder::write(unsigned char*, unsigned long)
Shadow bytes around the buggy address:
0x0c0e7fff8e80: fa fa fd fd fd fd fd fd fd fd fd fa fa fa fa fa
0x0c0e7fff8e90: 00 00 00 00 00 00 00 00 01 fa fa fa fa fa fd fd
0x0c0e7fff8ea0: fd fd fd fd fd fd fd fa fa fa fa fa fd fd fd fd
0x0c0e7fff8eb0: fd fd fd fd fd fd fa fa fa fa 00 00 00 00 00 00
0x0c0e7fff8ec0: 00 00 00 fa fa fa fa fa 00 00 00 00 00 00 00 00
=>0x0c0e7fff8ed0: 00 fa fa fa fa fa fd fd[fd]fd fd fd fd fd fd fd
0x0c0e7fff8ee0: fa fa fa fa fd fd fd fd fd fd fd fd fd fa fa fa
0x0c0e7fff8ef0: fa fa fd fd fd fd fd fd fd fd fd fa fa fa fa fa
0x0c0e7fff8f00: 00 00 00 00 00 00 00 00 01 fa fa fa fa fa fa fa
0x0c0e7fff8f10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
0x0c0e7fff8f20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
Addressable: 00
Partially addressable: 01 02 03 04 05 06 07
Heap left redzone: fa
Freed heap region: fd
Stack left redzone: f1
Stack mid redzone: f2
Stack right redzone: f3
Stack after return: f5
Stack use after scope: f8
Global redzone: f9
Global init order: f6
Poisoned by user: f7
Container overflow: fc
Array cookie: ac
Intra object redzone: bb
ASan internal: fe
Left alloca redzone: ca
Right alloca redzone: cb
==28751==ABORTING
The test program is a wrapper that I modified based on oss-fuzz wrapper:
// file name is qpdf_afl_fuzzer.c #include <qpdf/QPDF.hh> #include <qpdf/QPDFWriter.hh> #include <qpdf/QUtil.hh> #include <qpdf/BufferInputSource.hh> #include <qpdf/Buffer.hh> #include <qpdf/Pl_Discard.hh> #include <qpdf/QPDFPageDocumentHelper.hh> #include <qpdf/QPDFPageObjectHelper.hh> #include <qpdf/QPDFPageLabelDocumentHelper.hh> #include <qpdf/QPDFOutlineDocumentHelper.hh> #include <qpdf/QPDFAcroFormDocumentHelper.hh> #include <cstdlib>
class DiscardContents: public QPDFObjectHandle::ParserCallbacks { public: virtual ~DiscardContents() {} virtual void handleObject(QPDFObjectHandle) {} virtual void handleEOF() {} };
class FuzzHelper { public: FuzzHelper(char* data); void run();
private: PointerHolder<QPDF> getQpdf(); PointerHolder<QPDFWriter> getWriter(PointerHolder<QPDF>); void doWrite(PointerHolder<QPDFWriter> w); void testWrite(); void testPages(); void testOutlines(); void doChecks(); char const* input_f; Pl_Discard discard; };
FuzzHelper::FuzzHelper(char* _input) : // We do not modify data, so it is safe to remove the const for Buffer input_f(_input) { }
PointerHolder<QPDF> FuzzHelper::getQpdf() { PointerHolder<QPDF> qpdf = new QPDF(); qpdf->processFile(input_f); return qpdf; }
PointerHolder<QPDFWriter> FuzzHelper::getWriter(PointerHolder<QPDF> qpdf) { PointerHolder<QPDFWriter> w = new QPDFWriter(*qpdf); w->setOutputPipeline(&this->discard); w->setDecodeLevel(qpdf_dl_all); return w; }
void FuzzHelper::doWrite(PointerHolder<QPDFWriter> w) { try { w->write(); } catch (QPDFExc const& e) { std::cerr << e.what() << std::endl; } catch (std::runtime_error const& e) { std::cerr << e.what() << std::endl; } }
void FuzzHelper::testWrite() { // Write in various ways to exercise QPDFWriter
PointerHolder<QPDF\> q;
PointerHolder<QPDFWriter\> w;
q \= getQpdf();
w \= getWriter(q);
w\->setDeterministicID(true);
w\->setQDFMode(true);
doWrite(w);
q \= getQpdf();
w \= getWriter(q);
w\->setStaticID(true);
w\->setLinearization(true);
w\->setR6EncryptionParameters(
"u", "o", true, true, true, true, true, true, qpdf\_r3p\_full, true);
doWrite(w);
q \= getQpdf();
w \= getWriter(q);
w\->setStaticID(true);
w\->setObjectStreamMode(qpdf\_o\_disable);
w\->setR3EncryptionParameters(
"u", "o", true, true, qpdf\_r3p\_full, qpdf\_r3m\_all);
doWrite(w);
q \= getQpdf();
w \= getWriter(q);
w\->setDeterministicID(true);
w\->setObjectStreamMode(qpdf\_o\_generate);
w\->setLinearization(true);
doWrite(w);
}
void FuzzHelper::testPages() { // Parse all content streams, and exercise some helpers that // operate on pages. PointerHolder<QPDF> q = getQpdf(); QPDFPageDocumentHelper pdh(*q); QPDFPageLabelDocumentHelper pldh(*q); QPDFOutlineDocumentHelper odh(*q); QPDFAcroFormDocumentHelper afdh(*q); afdh.generateAppearancesIfNeeded(); pdh.flattenAnnotations(); std::vector<QPDFPageObjectHelper> pages = pdh.getAllPages(); DiscardContents discard_contents; int pageno = 0; for (std::vector<QPDFPageObjectHelper>::iterator iter = pages.begin(); iter != pages.end(); ++iter) { QPDFPageObjectHelper& page(*iter); ++pageno; try { page.coalesceContentStreams(); page.parsePageContents(&discard_contents); page.getPageImages(); pldh.getLabelForPage(pageno); QPDFObjectHandle page_obj(page.getObjectHandle()); page_obj.getJSON(true).unparse(); odh.getOutlinesForPage(page_obj.getObjGen());
std::vector<QPDFAnnotationObjectHelper\> annotations \=
afdh.getWidgetAnnotationsForPage(page);
for (std::vector<QPDFAnnotationObjectHelper\>::iterator annot\_iter \=
annotations.begin();
annot\_iter != annotations.end(); ++annot\_iter)
{
QPDFAnnotationObjectHelper& aoh \= \*annot\_iter;
afdh.getFieldForAnnotation(aoh);
}
}
catch (QPDFExc& e)
{
std::cerr << "page " << pageno << ": "
<< e.what() << std::endl;
}
}
}
void FuzzHelper::testOutlines() { PointerHolder<QPDF> q = getQpdf(); std::list<std::vector<QPDFOutlineObjectHelper> > queue; QPDFOutlineDocumentHelper odh(*q); queue.push_back(odh.getTopLevelOutlines()); while (! queue.empty()) { std::vector<QPDFOutlineObjectHelper>& outlines = *(queue.begin()); for (std::vector<QPDFOutlineObjectHelper>::iterator iter = outlines.begin(); iter != outlines.end(); ++iter) { QPDFOutlineObjectHelper& ol = *iter; ol.getDestPage(); queue.push_back(ol.getKids()); } queue.pop_front(); } }
void FuzzHelper::doChecks() { // Get as much coverage as possible in parts of the library that // might benefit from fuzzing. testWrite(); testPages(); testOutlines(); }
void FuzzHelper::run() { // The goal here is that you should be able to throw anything at // libqpdf and it will respond without any memory errors and never // do anything worse than throwing a QPDFExc or // std::runtime_error. Throwing any other kind of exception, // segfaulting, or having a memory error (when built with // appropriate sanitizers) will all cause abnormal exit. try { doChecks(); } catch (QPDFExc const& e) { std::cerr << "QPDFExc: " << e.what() << std::endl; } catch (std::runtime_error const& e) { std::cerr << "runtime_error: " << e.what() << std::endl; } }
int main(int argc, char** argv){ if (argc < 2){ std::cerr << "Please input the processed file!\n"; } FuzzHelper f(argv[1]); f.run(); return 0; }
The PoC file is attached:
poc.zip