Featured image of post CVE-2021-25786 QPDF PL_ASCII85DECODER::WRITE 漏洞分析报告

CVE-2021-25786 QPDF PL_ASCII85DECODER::WRITE 漏洞分析报告

CVE-2021-25786漏洞复现/POC/漏洞完整分析报告

CVE-2021-25786

漏洞简介

在QPDF版本10.0.4中发现了一个问题,允许远程攻击者通过在libqpdf的Pl_ASCII85Decoder::write参数中使用可陷入的.pdf文件来执行任意代码。

CVE-2021-25786 信息列表

描述项 属性值 说明
CWE-ID CWE-416 来源:NIST
CPE cpe:2.3:a:qpdf_project:qpdf:10.0.4:::::::* 来源:CVEdetails
发布日期 2023-08-11 14:15:12 来源:MITRE
最后更新 2023-09-27 16:16:12 来源:MITRE
影响产品 libqpdf -
影响版本 < 10.0.4 -
修复方法 升级至10.1.0及更高版本 -

CVE-2021-25786 CVSS 评分

基础分数 影响程度 CVSS向量 可被利用评分 影响因子 来源
5.3 中等 CVSS:3.1/AV:L/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:L 1.8 3.4 nvd@nist.gov

CVE-2021-25786 参考链接

描述 链接
Heap-use-after-free in Pl_ASCII85Decoder::write · Issue #492 · qpdf/qpdf · GitHub https://github.com/qpdf/qpdf/issues/492
[SECURITY] [DLA 3548-1] qpdf security update https://lists.debian.org/debian-lts-announce/2023/08/msg00037.html

CVE-2021-25786 漏洞PoC

CVE-2021-25786漏洞由基于oss-fuzzer开发的Fuzzer工具和AddressSanitizer(ASAN)触发,通过对libqpdf源码进行插桩,通过影子内存记录堆栈使用前的状态,校验堆栈使用后的状态,检测潜在的内存错误。Fuzzer工具的源码如下:

#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;
}

用于触发该CVE的PDF文件点击此处下载。 AddressSanitizer分析日志如下:

==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

AddressSanitizer工作机制详解

内存泄漏指的是在计算机程序中分配的内存空间在不再需要时未能被正确释放或回收的情况。简单来说,内存泄漏发生在程序中动态分配的内存没有被释放,导致这些内存无法再次使用,最终导致系统内存的消耗增加。内存泄漏并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,失去了对该段内存的控制,因而造成了内存的浪费。

AddressSanitizer(ASan)是一种内存错误检测工具,用于检测和报告程序中的内存安全问题,例如缓冲区溢出、使用未初始化的内存、使用已释放的内存等。它是由Google开发的,主要用于C和C++程序。 ASan通过在编译时对程序进行插桩(Instrumentation)/添加额外的运行时(Runtime Library)检查来实现内存错误检测。它会跟踪程序中的每个内存访问,并在发现潜在的内存错误时提供详细的报告。当程序访问已释放的内存、访问缓冲区边界之外的内存或者发生其它内存安全问题时,ASan会立即报告错误,指出错误发生的位置和具体信息,帮助开发人员快速定位和修复问题。 ASan还提供了一些额外的功能,例如检测堆栈溢出、检测使用未初始化的局部变量等。它可以与常见的编译器一起使用,如Clang和GCC,并且支持多个平台和操作系统。 使用AddressSanitizer可以帮助开发人员在早期发现和修复内存安全问题,提高程序的稳定性和安全性。然而,由于ASan的插桩和运行时检查会引入一些额外的开销,因此在生产环境中可能需要权衡性能和调试需求。

ASAN的插桩是在LLVM编译器级别对访问内存的操作(STORE/LOAD/ALLOCATE)进行处理。动态运行库提供复杂的功能(如Poison/Unpoison Shadow Memory/hook malloc/free等系统函数调用)。举个例子:若要堵上BufferOverflow漏洞,只需要在内存区域的右端(或两端,防Overflow和Underflow)加一块区域(RedZone),使RedZone区域的影子内存(Shadow Memory)设置为不可写。

内存错误类型表

错误类型 错误描述
Heap Used After Free 访问堆上已被释放的内存
Heap Buffer OverFlow 堆上缓冲区访问溢出
Stack Buffer OverFlow 栈上缓冲区访问溢出
Global Buffer OverFlow 全局缓冲区访问溢出
Use After Return 访问栈上已被释放的内存
Use After Scope 栈对象使用超过定义范围
Initialization Order Bugs 初始化命令错误
Memory Leaks 内存泄漏

“Heap-Used-After-Free” 分析报告

分析报告的内容由Fa1c0n与ydu016<ydu016@163.com>协作完成

内存错误类型ASAN实现时所使用的影子内存状态标志位对照表 以数值0xfa为例,当程序的影子内存状态被置为0xfa且被访问时,asan会触发内存访问异常且异常类型heap-buffer-overflow。 如上图所示,待修复的程序被asan判定漏洞类型为heap-use-after-free, 可以推断出, [1]待修复程序释放了一块内存空间,这导致影子内存标志位将这块内存空间标记为0xfd. [2]这块被释放且被标记的内存空间被使用. 我们准备调试验证这一点, 如上图所示,heap-use-after-free发生在pl_ascii85decoder.cc的85行, 源码如下 对应的反汇编如下(未插桩)

对应的反汇编如下(已插桩)

通过上图可以观察到,插桩后指令数量膨胀了不止两倍,多出的指令全部用于asan的内存检测。

通过上图可知,影子内存状态标志位由0x7fff8000+rsi的方式保存,其中0x7fff8000为基地址,rsi为偏移地址。

如上图所示,由0x7fff8000+rsi取出的影子内存状态标志位为0xfd,这说明根据,程序尝试访问一个已经被释放的内存空间,即heap-use-after-free,然而事实并没有这么简单。

由上图源码可知,内存访问错误的的inbuf被定义为数组,即使inbuf的数组空间在栈上开辟,也不需要对inbuf的数组空间进行释放操作,做为class的私有域,inbuf数组空间本的生命周期与inbuf所属的class保持同步。因此,不应存在heap-use-after-free,即inbuf数组堆空间被释放后再次被使用的情况。

另一个事实可以说明这个漏洞不是heap-use-after-free。如上图所示,patch在进行写操作前,将指针回退至起始位置,这是一个典型的堆溢出漏洞的修复方法。

为了验证这个结论,我们需要寻找将影子内存标志位由‘可用‘置为’0xfd‘的位置。

由于时间有限,没有具体定位到源码行,但我们将范围缩减至qpdfwrite.cc的3676行至3711行之间。同时,通过阅读源码我们可以断定,在3676行至3711行之间没有对inbuf进行包括堆释放在内的任何操作。因此可以证明,这个漏洞不是一个heap-use-after-free, 而是一个典型的堆溢出漏洞。