CVE-2019-13288漏洞分析

点击蓝字 关注我们

CVE-2019-13288漏洞分析




CVE-2019-13288

漏洞分析




漏洞涉及软件

CVE-2019-13288漏洞分析


CVE-2019-13288漏洞分析


版本早于4.01.01的Xpdf软件



/ 01 /

环境配置

操作系统:Ubuntu 20.04.2 LTS VMWare

Xpdf版本选择3.02


构建目标步骤

1. 为fuzz目标创建目录

cd $HOME  mkdir fuzzing_xpdf && cd fuzzing_xpdf/  

2. 下载一些额外工具(make和gcc)

sudo apt install build-essential  

3. 下载Xpdf 3.02:

wget https://dl.xpdfreader.com/old/xpdf-3.02.tar.gz   tar -xvzf xpdf-3.02.tar.gz  

4. 构建Xpdf

cd xpdf-3.02  sudo apt update && sudo apt install -y build-essential gcc  ./configure --prefix="$HOME/fuzzing_xpdf/install/"  make  make install 

5. 下载一些PDF样例用来测试构建是否成功

cd $HOME/fuzzing_xpdf  mkdir pdf_examples && cd pdf_examples  wget https://github.com/mozilla/pdf.js-sample-files/raw/master/helloworld.pdf   wget http://www.africau.edu/images/default/sample.pdf   wget https://www.melbpc.org.au/wp-content/uploads/2017/10/small-example-pdf-file.pdf   

6. 测试构建结果

$HOME/fuzzing_xpdf/install/bin/pdfinfo -box -meta   $HOME/fuzzing_xpdf/pdf_examples/helloworld.pdf  

结果应如下图所示:

CVE-2019-13288漏洞分析


下载AFL++



1. 下载AFL++

sudo apt-get update  sudo apt-get install -y build-essential python3-dev automake git flex bison libglib2.0-dev libpixman-1-dev python3-setuptools  sudo apt-get install -y lld-11 llvm-11 llvm-11-dev clang-11 || sudo apt-get install -y lld llvm llvm-dev clang  sudo apt-get install -y gcc-$(gcc --version|head -n1|sed 's/.* //'|sed 's/..*//')-plugin-dev libstdc++-$(gcc --version|head -n1|sed 's/.* //'|sed 's/..*//')-dev  

2. 构建AFL++

cd $HOME  git clone https://github.com/AFLplusplus/AFLplusplus  && cd AFLplusplus  export LLVM_CONFIG="llvm-config-11"  make distrib  sudo make install  

3. 测试构建结果

 afl-fuzz  

结果应如下图所示:

CVE-2019-13288漏洞分析
CVE-2019-13288漏洞分析



/ 02 /

实际操作


对目标进行fuzz

AFL 是一个覆盖引导的模糊器(coverage-guided fuzzer),这意味着它收集每个变异输入的覆盖信息,来发现新的执行路径和潜在的错误。当源代码可用时,AFL 可以使用插桩(instrumentation),在每个基本块(函数、循环等)的开头插入函数调用。

要为我们的目标程序启用检测,我们需要使用 AFL 的编译器编译源代码。

1. 清理所有之前编译的目标文件和可执行文件

rm -r $HOME/fuzzing_xpdf/install  cd $HOME/fuzzing_xpdf/xpdf-3.02/  make clean 

2. 使用 afl-clang-fast 编译器构建 xpdf

export LLVM_CONFIG="llvm-config-11"  CC=$HOME/AFLplusplus/afl-clang-fast CXX=$HOME/AFLplusplus/afl-clang-fast++ ./configure --prefix="$HOME/fuzzing_xpdf/install/"  make  make install  

3. 使用以下命令对目标“pdftotext”进行fuzz

afl-fuzz -i $HOME/fuzzing_xpdf/pdf_examples/ -o $HOME/fuzzing_xpdf/out/ -s 123 --$HOME/fuzzing_xpdf/install/bin/pdftotext @@$HOME/fuzzing_xpdf/output 

命令解释:

(1) – i 表示我们需要放置输入示例的目录

(2) – o 表示AFL++将会把mutated文件放入的目录

(3) – s 表示将会使用的静态随机种子

(4) @@ 是命令行中的占位符号,AFL++会使用每个测试的输入文件名替换该占位符号

所以基本上,AFL++ fuzzer将会对每一个输入文件运行命令:

$HOME/fuzzing_xpdf/install/bin/pdftotext <input-file-name> $HOME/fuzzing_xpdf/output 

如果在运行fuzzer的时候显示“Hmm, your system is configured to send core dump notifications to an external utility…”字样的报错,则运行以下命令后重试上述步骤3。

sudo su  echo core >/proc/sys/kernel/core_pattern  exit

报错内容如下图所示:

CVE-2019-13288漏洞分析

在运行上述步骤3成功并fuzz一段时间后,会看到如下图所示界面:

CVE-2019-13288漏洞分析

图中红色字样代表已发现crashes数量。(在本次fuzz中,发现的第一个crash文件就是我们要复现CVE漏洞所要找的崩溃文件)在发现crash后,该文件会被储存在$HOME/fuzzing_xpdf/out/default/crashes目录下,文件名类似于” id:000000,sig:11,src:000001,time:208929,execs:224806,op:havoc,rep:1″


崩溃复现



使用以下命令将此崩溃文件作为输入文件传递给pdftotext。

$HOME/fuzzing_xpdf/install/bin/pdftotext '/home/fuzz/fuzzing_xpdf/out/default/crashes/<your_filename>' $HOME/fuzzing_xpdf/output

会显示软件崩溃报错,如下图:

CVE-2019-13288漏洞分析

调试

使用gdb来调试找出该程序因此输入文件而崩溃的原因。

1. 使用如下命令来使用调试信息重新构造Xpdf以获取符号堆栈跟踪。

rm -r $HOME/fuzzing_xpdf/install  cd $HOME/fuzzing_xpdf/xpdf-3.02/  make clean  CFLAGS="-g -O0" CXXFLAGS="-g -O0" ./configure --prefix="$HOME/fuzzing_xpdf/install/"  make  make install  

2. 使用如下命令运行gdb,在gdb中输入命令”run”。

gdb --args $HOME/fuzzing_xpdf/install/bin/pdftotext $HOME/fuzzing_xpdf/out/default/crashes/<your_filename> $HOME/fuzzing_xpdf/output 

得到了如下图所示的输出结果:

CVE-2019-13288漏洞分析

3. 在gdb中输入命令”bt”来获取回溯信息,如下图:

CVE-2019-13288漏洞分析

注:如果获取的回溯信息过少可以在输入命令”bt”获取回溯信息后再输入命令”ret”获取更多信息。

分析我们获得的回溯信息可以发现,程序在崩溃前一直在不停地循环调用”Parser::getObj”<-“XRef::fetch”<-“Object::fetch”<-“Dict::lookup”<-“Object::dictlookup”<-“Parser::makeStream”<-“Parser::getObj”这条函数调用链,代表该崩溃是由于一个无限递归的循环导致的。正如https://www.cvedetails.com/cve/CVE-2019-13288中描述的一样。


使用wpwndbg

分析漏洞成因

1. 使用命令”b main”在main函数下断点,然后运行进程。

CVE-2019-13288漏洞分析

2. 一直步过观察程序运行过程,发现程序在运行到代码第237行”doc→displayPages”函数后步过会报错,可以确认这个是问题函数,因此使用命令”b *(0x55555562250e)”在这里下断点方便后续进行调试。

CVE-2019-13288漏洞分析
CVE-2019-13288漏洞分析

3. 步入”doc→displayPages”函数,继续步过观察运行过程,发现在运行”displayPage”函数后报错,所以重复上述类似操作,先下断点,然后继续寻找问题点。

CVE-2019-13288漏洞分析
CVE-2019-13288漏洞分析

4. 步入”displayPage”函数,发现运行“catalog→getPage(page)→display”函数后报错,重复类似操作,不再详细描述。

CVE-2019-13288漏洞分析

5.步入“catalog→getPage(page)→display”函数,类似操作定位到“Page *getPage”函数。

CVE-2019-13288漏洞分析

6. 步入“Page *getPage”函数,类似操作定位到“displaySlice”函数。

CVE-2019-13288漏洞分析

7. 步入找到“displaySlice”函数,类似操作定位到“contents.fetch”函数。

CVE-2019-13288漏洞分析

8. 步入“contents.fetch”函数后,发现报错中的循环函数出现。查看”contents”发现它是一个objRef类型的对象,这里有两个值num = 7,gen = 0。

CVE-2019-13288漏洞分析

9. 步入“contents.fetch”函数,运行到以下位置,发现num和gen被传入”xref→fetch”函数中。此时步过该函数会崩溃报错。

CVE-2019-13288漏洞分析

10.我们步入”xref→fetch”函数后继续步过,发现在”parser→getObj”函数,步过”parser→getObj”函数发现该函数会一直循环调用。至此进入我们在崩溃复现中查看栈帧时发现的循环调用逻辑。

CVE-2019-13288漏洞分析

11. 我们从源码层面观察一下函数,从崩溃复现中我们可以得知循环是由”Parser::makeStream”<-“Parser::getObj”这步导致的,此时”Parser::getObj”函数参数为(obj,NULL,RC4,7,0)。

Parser::getObj代码:

Object *Parser::getObj(Object *obj, Guchar *fileKey,                 CryptAlgorithm encAlgorithm, int keyLength,                 int objNum, int objGen) {    char *key;    Stream *str;    Object obj2;    int num;    DecryptStream *decrypt;    GString *s, *s2;     int c;         // refill buffer after inline image data     if (inlineImg == 2) {       buf1.free();       buf2.free();       lexer->getObj(&buf1);       lexer->getObj(&buf2);       inlineImg = 0;     }         // array     if (buf1.isCmd("[")) {       shift();       obj->initArray(xref);       while (!buf1.isCmd("]") && !buf1.isEOF())         obj->arrayAdd(getObj(&obj2, fileKey, encAlgorithm, keyLength,                  objNum, objGen));       if (buf1.isEOF())         error(getPos(), "End of file inside array");       shift();         // dictionary or stream     } else if (buf1.isCmd("<<")) {       shift();       obj->initDict(xref);       while (!buf1.isCmd(">>") && !buf1.isEOF()) {         if (!buf1.isName()) {       error(getPos(), "Dictionary key must be a name object");       shift();         } else {       key = copyString(buf1.getName());       shift();       if (buf1.isEOF() || buf1.isError()) {         gfree(key);         break;       }       obj->dictAdd(key, getObj(&obj2, fileKey, encAlgorithm, keyLength,                    objNum, objGen));         }       }       if (buf1.isEOF())         error(getPos(), "End of file inside dictionary");       // stream objects are not allowed inside content streams or       // object streams       if (allowStreams && buf2.isCmd("stream")) {         if ((str = makeStream(obj, fileKey, encAlgorithm, keyLength,                   objNum, objGen))) {       obj->initStream(str);         } else {       obj->free();       obj->initError();         }       } else {         shift();       }         // indirect reference or integer     } else if (buf1.isInt()) {       num = buf1.getInt();       shift();       if (buf1.isInt() && buf2.isCmd("R")) {         obj->initRef(num, buf1.getInt());         shift();         shift();       } else {         obj->initInt(num);       }         // string     } else if (buf1.isString() && fileKey) {       s = buf1.getString();       s2 = new GString();       obj2.initNull();       decrypt = new DecryptStream(new MemStream(s->getCString(), 0,                             s->getLength(), &obj2),                   fileKey, encAlgorithm, keyLength,                   objNum, objGen);       decrypt->reset();       while ((c = decrypt->getChar()) != EOF) {         s2->append((char)c);       }       delete decrypt;       obj->initString(s2);       shift();         // simple object     } else {       buf1.copy(obj);       shift();     }          return obj;    }   

12.调用“Parser::makeStream”函数后,参数基本相同,这里的*dict就是我们”contents”中传入的obj,那么此时”dict->dictLookup”的意思应该是从我们传入的obj中查询”Length”对应的值(value),继续更进“dictLookup”

Parser::makeStream代码:

Stream *Parser::makeStream(Object *dict, Guchar *fileKey,                 CryptAlgorithm encAlgorithm, int keyLength,                 int objNum, int objGen) {    Object obj;    BaseStream *baseStr;    Stream *str;    Guint pos, endPos, length;       // get stream start position    lexer->skipToNextLine();    pos = lexer->getPos();       // get length    dict->dictLookup("Length", &obj);    if (obj.isInt()) {      length = (Guint)obj.getInt();      obj.free();    } else {      error(getPos(), "Bad 'Length' attribute in stream");      obj.free();      return NULL;    }  

14.查看“dict::lookup”函数,此时参数仍为(value,obj),可以看到e->val.fetch(xref,obj)函数。

Dict::lookup代码:

inline Object *Object::dictLookup(char *key, Object *obj)    { return dict->lookup(key, obj); }  

15.使用动态调试,将断点下在执行完”find”函数之后,此时查看*e的值,也就是“e→val”的值,发现*e是一个objRef类型的对象,并且发现它的ref数组中num = 7,gen = 0,与”contents”一样,那么因此,在再次调用”fetch”函数后,函数调用形成闭环。

CVE-2019-13288漏洞分析
CVE-2019-13288漏洞分析



/ 03 /

总结

1. Main函数调用链为”main”->”displayPages”->”displayPage”->”getPage”->”displaySlice”→”contents.fetch”

2. contents是类型为objRef的对象,其中有一个ref数组,值为(num=7,gen=0)

3. 调用xref->fetch(ref.num, ref.gen, obj)

4. “xref→fetch”->”parser→getObj”->”Parser::makeStream”->”Object::dictlookup”->”Dict::lookup”->”Object::fetch”->”XRef::fetch”→”Parser::getObj”开始循环。

5. “Dict::lookup”是问题出现点,原因是find函数寻找“Length”值与“contents”值相同。

CVE-2019-13288漏洞分析



CVE-2019-13288漏洞分析

【END】



往期精彩合集



● Windows中压缩包可能出现的安全问题及相关缓解方案参考

● 五分钟了解安全万亿赛道——机密计算

● 移动操作系统的底层安全机制

● pixel5内核build、pixel6 LineageOS编译、motorola救砖

● 智能应用控制和目录签名

● 前后端分离架构下 利用SpringBoot确保接口安全性

● 浅谈如何防止他人恶意刷接口

● 联想全球安全实验室受邀参加XCon2024安全技术峰会,应“适者生存”的丛林法则,分享《近源渗透之攻守道》

● 大模型文件的中毒攻击与防护

● 探索FPGA的xclbin文件格式


联想GIC全球安全实验室(中国)

[email protected]


CVE-2019-13288漏洞分析


原文始发于微信公众号(联想全球安全实验室):CVE-2019-13288漏洞分析

版权声明:admin 发表于 2024年11月8日 下午3:21。
转载请注明:CVE-2019-13288漏洞分析 | CTF导航

相关文章