CVE-2019-13288 xpdf 无限递归

CVE-2019-13288

xpdf-3.02.
放两个最简单的pdf作为input,-s 123开跑.5秒后出现第一个crash.

崩溃分析

用第一个crash样例,复现一下

gdb启动,直接跑到奔溃点.可以看出是rsp到了不可写的内存区域.

backtrace一下,发现栈回溯有十万条,结合rsp到了不可写区域,可以推理出是无限递归导致的段错误.
backtrace -40查看最开始的40条记录.发现绿色框中的一段调用序列在不断重复,这就是无限递归的调用序列.

漏洞分析

结合回溯时的源码信息跟一下源码.在此之前需要对pdf格式有一个大概的了解:
https://blog.csdn.net/tjcwt2011/article/details/107877566
https://zxyle.github.io/PDF-Explained/
遇到具体的问题再参照pdf1.7标准参考

由于触发路径不长,可以直接从main函数开始跟,大概了解一下整个程序的流程.
发现触发时已经完成对pdf文件的基本解析,正在准备输出text文件.

1
2
3
4
5
6
7
8
9
10
11
12
// write text file
textOut = new TextOutputDev(textFileName->getCString(),
physLayout, rawOrder, htmlMeta);
if (textOut->isOk()) {
doc->displayPages(textOut, firstPage, lastPage, 72, 72, 0,
gFalse, gTrue, gFalse);
} else {
delete textOut;
exitCode = 2;
goto err3;
}
delete textOut;

一些dispatch:…

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
void PDFDoc::displayPages(OutputDev *out, int firstPage, int lastPage,
double hDPI, double vDPI, int rotate,
GBool useMediaBox, GBool crop, GBool printing,
GBool (*abortCheckCbk)(void *data),
void *abortCheckCbkData) {
int page;

for (page = firstPage; page <= lastPage; ++page) {
displayPage(out, page, hDPI, vDPI, rotate, useMediaBox, crop, printing,
abortCheckCbk, abortCheckCbkData);
}
}

void PDFDoc::displayPage(OutputDev *out, int page,
double hDPI, double vDPI, int rotate,
GBool useMediaBox, GBool crop, GBool printing,
GBool (*abortCheckCbk)(void *data),
void *abortCheckCbkData) {
if (globalParams->getPrintCommands()) {
printf("***** page %d *****\n", page);
}
catalog->getPage(page)->display(out, hDPI, vDPI,
rotate, useMediaBox, crop, printing, catalog,
abortCheckCbk, abortCheckCbkData);
}

void Page::display(OutputDev *out, double hDPI, double vDPI,
int rotate, GBool useMediaBox, GBool crop,
GBool printing, Catalog *catalog,
GBool (*abortCheckCbk)(void *data),
void *abortCheckCbkData) {
displaySlice(out, hDPI, vDPI, rotate, useMediaBox, crop,
-1, -1, -1, -1, printing, catalog,
abortCheckCbk, abortCheckCbkData);
}

经过分发dispatch之后,最后的输出由displaySlice实现,这里也是调用环的入口点.

1
2
3
4
5
6
void Page::displaySlice(...)
{
...
contents.fetch(xref, &obj);
...
}

先来看一下Object::fetch函数,从三目运算符中可以看出,这个函数是在传入的obj上构造当前对象.由于pdf格式中存在的Indirect Objects类型有不同的处理.如果是普通类型,则直接复制当前对象到obj.如果是间接对象,则在xref表中查找并再次fetch当前对象的实例.

1
2
3
4
Object *Object::fetch(XRef *xref, Object *obj) {
return (type == objRef && xref) ?
xref->fetch(ref.num, ref.gen, obj) : copy(obj);
}

本用例中,contents是一个间接对象

XRef::fetch的逻辑如下:

  • 根据num在XREF表中找到当前间接对象对应实例的条目e.
    • 如果是未压缩的条目
      • 启动一个新的parser从pdf文件中该实例起始偏移开始parse.
      • 先读入obj1,obj2,obj3(分别对应pdf对象语法中的num,gen,obj/R)
      • 再调用parser->getObj对该实例主体部分进行parse来构造obj.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
Object *XRef::fetch(int num, int gen, Object *obj) {
XRefEntry *e;
Parser *parser;
Object obj1, obj2, obj3;

// check for bogus ref - this can happen in corrupted PDF files
if (num < 0 || num >= size) {
goto err;
}

e = &entries[num];
switch (e->type) {

case xrefEntryUncompressed:
if (e->gen != gen) {
goto err;
}
obj1.initNull();
parser = new Parser(this,
new Lexer(this,
str->makeSubStream(start + e->offset, gFalse, 0, &obj1)),
gTrue);
parser->getObj(&obj1);
parser->getObj(&obj2);
parser->getObj(&obj3);
if (!obj1.isInt() || obj1.getInt() != num ||
!obj2.isInt() || obj2.getInt() != gen ||
!obj3.isCmd("obj")) {
obj1.free();
obj2.free();
obj3.free();
delete parser;
goto err;
}
parser->getObj(obj, encrypted ? fileKey : (Guchar *)NULL,
encAlgorithm, keyLength, num, gen);
obj1.free();
obj2.free();
obj3.free();
delete parser;
break;

只关注该样例中触发的路径.本样例中,contents是编号为7的stream对象的间接对象.

下方是省略后的Parser::getObj.在第30行中构造以name对象”Lenth”为key的字典条目时,再次调用了getObj函数来获得该条目的value.

第二次层调用会直接来到第51行,识别出该value是一个间接对象.这是符合pdf标准的.

Any object in a PDF file may be labeled as an indirect object.

回到第一层调用,第38行,此时stream对象的dictionary部分已经parse完毕,调用makeStream进行主体部分的解析.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
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;


......

// dictionary or stream
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);
}

return obj;
}

解析主体部分需要先获取主体部分的长度,即之前字典中保存的Lenth对应的value.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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;
}

如之前所述,由于允许value是一个间接对象,所以这里需要调用Object::fetch来尝试获得该间接对象的实例.

1
2
3
4
5
Object *Dict::lookup(char *key, Object *obj) {
DictEntry *e;

return (e = find(key)) ? e->val.fetch(xref, obj) : obj->initNull();
}

结合我们的输入,该Length对应的间接对象即是该stream对象本身,于是我们又开始了新一轮对该stream对象的解析,至此形成闭环,造成无限递归.

漏洞总结

跟完漏洞的触发链后,来分析一下造成该漏洞的原因.
在源码分析的过程中,我潜意识中一直错误地认为是”间接对象的实例也是一个间接对象且不断循环”造成的问题,并认为既然标准这样规定,这个问题似乎无法避免.但其实细想一下,根本无法构造间接对象的实例也是一个间接对象的情况,因为间接对象不存在对象编号,也就无法被引用.
且在解引用(XRef::fetch)的过程中,也有对间接对象的实例必须是普通对象的check.

1
2
3
4
5
6
7
8
9
   if (!obj1.isInt() || obj1.getInt() != num ||
!obj2.isInt() || obj2.getInt() != gen ||
!obj3.isCmd("obj")) { // ! ! !
obj1.free();
obj2.free();
obj3.free();
delete parser;
goto err;
}

所以漏洞实际的成因是,在解析A(stream)对象的过程中,需要解析间接对象B(value for “Lenth”),而在解析间接对象B的过程中,需要解析对象A(stream).倒是一个非常常见的无限循环的Pattern了(比如循环继承,比如文法中的左递归).

漏洞修补

看到的一种修补方案是:

在研究清楚为什么 crash 之后,我们的修复方案已经明了:若 Length 所对应的 val 不是 objInt,则不能执行第 6 条分析里的 obj->dictLookup(“Length”, &newobj)

这其实并不符合pdf关于间接对象的标准.

官方的修补是这样的(源码来自xpdf-4.05),加入了recursion的限制,如果递归层数达到500就不会再进一步对stream解析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
  // If object is a Ref, fetch and return the referenced object.
// Otherwise, return a copy of the object.
Object *fetch(XRef *xref, Object *obj, int recursion = 0);


Object *Parser::getObj(Object *obj, GBool simpleOnly,
Guchar *fileKey,
CryptAlgorithm encAlgorithm, int keyLength,
int objNum, int objGen, int recursion) {
......
if (!simpleOnly && recursion < recursionLimit && buf1.isCmd("<<"))
......
}


而是当作simple object,直接从buf1的token中构造对象.此时buf1为待解析的objCmd:”>>”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// simple object
} else {
buf1.copy(obj);
shift();
}



Object *Object::copy(Object *obj) {
*obj = *this;
switch (type) {
case objString:
obj->string = string->copy();
break;
case objName:
obj->name = copyString(name);
break;
case objArray:
array->incRef();
break;
case objDict:
dict->incRef();
break;
case objStream:
obj->stream = stream->copy();
break;
case objCmd:
obj->cmd = copyString(cmd);
break;
default:
break;
}

然后会由于对Lenth的解析没有解析出一个objInt而抛出错误Missing or invalid ‘Length’ attribute in stream.然后就是解递归的过程.

1
2
3
4
5
6
7
8
9
10
} else {
Object obj;
dict->dictLookup("Length", &obj, recursion);
if (obj.isInt()) {
length = (GFileOffset)(Guint)obj.getInt();
haveLength = gTrue;
} else {
error(errSyntaxError, getPos(),
"Missing or invalid 'Length' attribute in stream");
}

  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2022-2024 翰青HanQi

请我喝杯咖啡吧~

支付宝
微信