点击蓝字 关注我们
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
结果应如下图所示:
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
结果应如下图所示:
/ 02 /
实际操作
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
报错内容如下图所示:
在运行上述步骤3成功并fuzz一段时间后,会看到如下图所示界面:
图中红色字样代表已发现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
会显示软件崩溃报错,如下图:
调试
使用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
得到了如下图所示的输出结果:
3. 在gdb中输入命令”bt”来获取回溯信息,如下图:
注:如果获取的回溯信息过少可以在输入命令”bt”获取回溯信息后再输入命令”ret”获取更多信息。
分析我们获得的回溯信息可以发现,程序在崩溃前一直在不停地循环调用”Parser::getObj”<-“XRef::fetch”<-“Object::fetch”<-“Dict::lookup”<-“Object::dictlookup”<-“Parser::makeStream”<-“Parser::getObj”这条函数调用链,代表该崩溃是由于一个无限递归的循环导致的。正如https://www.cvedetails.com/cve/CVE-2019-13288中描述的一样。
1. 使用命令”b main”在main函数下断点,然后运行进程。
2. 一直步过观察程序运行过程,发现程序在运行到代码第237行”doc→displayPages”函数后步过会报错,可以确认这个是问题函数,因此使用命令”b *(0x55555562250e)”在这里下断点方便后续进行调试。
3. 步入”doc→displayPages”函数,继续步过观察运行过程,发现在运行”displayPage”函数后报错,所以重复上述类似操作,先下断点,然后继续寻找问题点。
4. 步入”displayPage”函数,发现运行“catalog→getPage(page)→display”函数后报错,重复类似操作,不再详细描述。
5.步入“catalog→getPage(page)→display”函数,类似操作定位到“Page *getPage”函数。
6. 步入“Page *getPage”函数,类似操作定位到“displaySlice”函数。
7. 步入找到“displaySlice”函数,类似操作定位到“contents.fetch”函数。
8. 步入“contents.fetch”函数后,发现报错中的循环函数出现。查看”contents”发现它是一个objRef类型的对象,这里有两个值num = 7,gen = 0。
9. 步入“contents.fetch”函数,运行到以下位置,发现num和gen被传入”xref→fetch”函数中。此时步过该函数会崩溃报错。
10.我们步入”xref→fetch”函数后继续步过,发现在”parser→getObj”函数,步过”parser→getObj”函数发现该函数会一直循环调用。至此进入我们在崩溃复现中查看栈帧时发现的循环调用逻辑。
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”函数后,函数调用形成闭环。
/ 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”值相同。
【END】
往期精彩合集
● Windows中压缩包可能出现的安全问题及相关缓解方案参考
● pixel5内核build、pixel6 LineageOS编译、motorola救砖
● 前后端分离架构下 利用SpringBoot确保接口安全性
长
按
关
注
联想GIC全球安全实验室(中国)
原文始发于微信公众号(联想全球安全实验室):CVE-2019-13288漏洞分析