CVE-2017-9047(libxml2) 분석
개요
xml(EXtensible Markup Language) : 여러 특수 목적의 마크업 언어를 만드는 용도로 사용되는 다목적 마크업 언어
libxml2 : xml 파싱 라이브러리
- CVE-2017-9047 : libxml2 2.9.4,
xmlSnprintfElementContent
함수에서 발생하는stack-based buffer overflow
취약점 - CVE-2017-9048 : 위와 동일
AFL - 딕셔너리
대상 프로그램이 복잡한 텍스트 기반 파일 포맷(예: xml)을 입력으로 받는다면 퍼저에게 기본적인 syntax 토큰에 대한 딕셔너리를 제공해주는게 좋다.
AFL은 딕셔너리를 이용하여 테스트케이스를 변경하며, 다음과 같은 작업을 수행한다.
- override : 특정 위치를 n 바이트로 바꾼다. 여기서 n은 딕셔너리 엔트리의 길이이다.
- insert : 현재 파일의 위치에 딕셔너리 엔트리를 삽입하며, 강제로 파일 내용을 n 만큼 이동하고 파일 크기를 늘린다.
AFL - 병렬화
병렬화 방식은 두가지 방법이 있는데 각각의 장단점이 있으니 골라서 사용하면 될 거 같다.
독립 인스턴스
완전히 별개인 AFL을 실행한다.
AFL은 비결정론적 테스트 알고리즘( Exercise 1 - Xpdf 참고)을 사용하기 때문에 더 많은 인스턴스를 실행할 수록 성공 확률이 높아진다.
-s 옵션을 사용하는 경우 인스턴스마다 다른 시드를 사용해야 한다.
공유 인스턴스
공유 인스턴스가 병렬 퍼징에 대해 더 나은 접근 방식이라고 한다.
해당 방식은 각 인스턴스가 다른 인스턴스에서 찾은 테스트케이스를 수집하고 사용한다.
AFL의 -M 옵션과 -S 옵션으로 하나의 마스터 인스턴스와 n개의 슬레이브 인스턴스를 사용할 수 있다.
- 마스터 인스턴스
1
./afl-fuzz -i afl_in -o afl_out -M Master -- ./program @@
- 슬레이브 인스턴스
1
2
3
4
./afl-fuzz -i afl_in -o afl_out -S Slave1 -- ./program @@
./afl-fuzz -i afl_in -o afl_out -S Slave2 -- ./program @@
...
./afl-fuzz -i afl_in -o afl_out -S SlaveN -- ./program @@
퍼저 실행
퍼저를 실행하기 전, 몇 가지 준비사항이 있다.
- XML 문법 딕셔너리 생성
AFL++에서는 다양한 파일 포맷에 대한 딕셔너리를 제공해준다.
XML 딕셔너리를 다운로드 해주었다.
1
wget https://raw.githubusercontent.com/AFLplusplus/AFLplusplus/stable/dictionaries/xml.dict
- AFL input 파일 생성
해당 Exercise에서는 제공되는 input 파일을 사용하라고 했으므로 해당 파일을 다운로드 해주었다.
1
wget https://github.com/antonio-morales/Fuzzing101/raw/main/Exercise%205/SampleInput.xml
- AFL 실행
- 마스터 인스턴스 실행
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
afl-fuzz -m none -i ./afl_in -o afl_out -s 123 -x ./dict/xml.dict -D -M master -- ./install/bin/xmllint --memory --noenc --nocdata --dtdattr --loaddtd --valid --xinclude @@
```
- 슬레이브 인스턴스 실행
```bash
afl-fuzz -m none -i ./afl_in -o afl_out -s 234 -S slave1 -- ./install/bin/xmllint --memory --noenc --nocdata --dtdattr --loaddtd --valid --xinclude @@
```
- 인자 설명
- -M, -S 옵션 뒤에 오는 값은 해당 인스턴스의 아이디이다.
- -x 옵션으로 퍼저에게 딕셔너리를 제공할 수 있다.
- 커버리지를 넓히기 위해 다양한 프로그램 옵션을 주었다.
<br>
1개의 마스터 인스턴스와 3개의 슬레이브 인스턴스를 실행하였다.
![1](/assets/img/2023-03-20-CVE-2017-9047(libxml2)-분석.md/1.png)
## 크래시 분석
---
`xmlSnprintfElementContent` 함수에서 스택 버퍼 오버플로우가 발생하였다.
![2](/assets/img/2023-03-20-CVE-2017-9047(libxml2)-분석.md/2.png)
<br>
퍼저에게 주었던 여러 옵션 중에서 크래시가 터지는 옵션을 찾게 되어 해당 옵션을 주고 콜스택을 따라 분석을 진행하였다.
```bash
./install/bin/xmllint --valid crash.xml
—-valid
옵션은 문서의 유효성을 검사하는 옵션이다.
먼저 메인함수에서는 옵션으로 주어진 인자를 파싱한 후 인자로 전달된 파일을 연 후 parseAndPrintFile
함수를 호출한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#ifdef LIBXML_READER_ENABLED
if (stream != 0)
streamFile(argv[i]);
else
#endif /* LIBXML_READER_ENABLED */
if (sax) {
testSAX(argv[i]);
} else {
parseAndPrintFile(argv[i], NULL);
}
if ((chkregister) && (nbregister != 0)) {
fprintf(stderr, "Registration count off: %d\n", nbregister);
progresult = XMLLINT_ERR_RDREGIS;
}
}
files ++;
if ((timing) && (repeat)) {
endTimer("%d iterations", repeat);
}
}
해당 함수에서는 여러 옵션에 대한 로직을 실행한다.
valid 옵션이 켜져있으므로 valid 옵션에 대한 로직을 수행한다.
파싱한 데이터를 가져올 컨텍스트를 생성하고 파일을 읽어온다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#ifdef LIBXML_VALID_ENABLED
} else if (valid) {
xmlParserCtxtPtr ctxt = NULL;
if (rectxt == NULL)
ctxt = xmlNewParserCtxt();
else
ctxt = rectxt;
if (ctxt == NULL) {
doc = NULL;
} else {
doc = xmlCtxtReadFile(ctxt, filename, NULL, options);
if (ctxt->valid == 0)
progresult = XMLLINT_ERR_RDFILE;
if (rectxt == NULL)
xmlFreeParserCtxt(ctxt);
}
xmlCtxtReadFile
함수는 parser를 초기화한 후 실질적인 read를 진행하는 xmlDoRead
함수를 호출한다.
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
/**
* xmlCtxtReadFile:
* @ctxt: an XML parser context
* @filename: a file or URL
* @encoding: the document encoding, or NULL
* @options: a combination of xmlParserOption
*
* parse an XML file from the filesystem or the network.
* This reuses the existing @ctxt parser context
*
* Returns the resulting document tree
*/
xmlDocPtr
xmlCtxtReadFile(xmlParserCtxtPtr ctxt, const char *filename,
const char *encoding, int options)
{
xmlParserInputPtr stream;
if (filename == NULL)
return (NULL);
if (ctxt == NULL)
return (NULL);
xmlInitParser();
xmlCtxtReset(ctxt);
stream = xmlLoadExternalEntity(filename, NULL, ctxt);
if (stream == NULL) {
return (NULL);
}
inputPush(ctxt, stream);
return (xmlDoRead(ctxt, NULL, encoding, options, 1));
}
xmlDoRead
함수는 파싱을 담당하는 xmlParseDocument
함수를 호출한다.
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
/**
* xmlDoRead:
* @ctxt: an XML parser context
* @URL: the base URL to use for the document
* @encoding: the document encoding, or NULL
* @options: a combination of xmlParserOption
* @reuse: keep the context for reuse
*
* Common front-end for the xmlRead functions
*
* Returns the resulting document tree or NULL
*/
static xmlDocPtr
xmlDoRead(xmlParserCtxtPtr ctxt, const char *URL, const char *encoding,
int options, int reuse)
{
xmlDocPtr ret;
xmlCtxtUseOptionsInternal(ctxt, options, encoding);
if (encoding != NULL) {
xmlCharEncodingHandlerPtr hdlr;
hdlr = xmlFindCharEncodingHandler(encoding);
if (hdlr != NULL)
xmlSwitchToEncoding(ctxt, hdlr);
}
if ((URL != NULL) && (ctxt->input != NULL) &&
(ctxt->input->filename == NULL))
ctxt->input->filename = (char *) xmlStrdup((const xmlChar *) URL);
xmlParseDocument(ctxt);
if ((ctxt->wellFormed) || ctxt->recovery)
ret = ctxt->myDoc;
else {
ret = NULL;
if (ctxt->myDoc != NULL) {
xmlFreeDoc(ctxt->myDoc);
}
}
ctxt->myDoc = NULL;
if (!reuse) {
xmlFreeParserCtxt(ctxt);
}
return (ret);
}
ParseDocument
함수는 xml 문서를 파싱한다.
xmlParseElement
함수에서 오류가 발생했으므로 해당 함수를 분석하였다.
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
/**
* xmlParseDocument:
* @ctxt: an XML parser context
*
* parse an XML document (and build a tree if using the standard SAX
* interface).
*
* [1] document ::= prolog element Misc*
*
* [22] prolog ::= XMLDecl? Misc* (doctypedecl Misc*)?
*
* Returns 0, -1 in case of error. the parser context is augmented
* as a result of the parsing.
*/
int
xmlParseDocument(xmlParserCtxtPtr ctxt) {
.
.
/*
* Time to start parsing the tree itself
*/
GROW;
if (RAW != '<') {
xmlFatalErrMsg(ctxt, XML_ERR_DOCUMENT_EMPTY,
"Start tag expected, '<' not found\n");
} else {
ctxt->instate = XML_PARSER_CONTENT;
xmlParseElement(ctxt);
ctxt->instate = XML_PARSER_EPILOG;
.
.
}
Element를 파싱하는 과정 중 비어있는 Element를 확인하는 루틴이 존재한다.
ctxt→sax→endElementNs
에는 xmlSAX2EndElementNs
함수가 들어있다.
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
/**
* xmlParseElement:
* @ctxt: an XML parser context
*
* parse an XML element, this is highly recursive
*
* [39] element ::= EmptyElemTag | STag content ETag
*
* [ WFC: Element Type Match ]
* The Name in an element's end-tag must match the element type in the
* start-tag.
*
*/
void
xmlParseElement(xmlParserCtxtPtr ctxt) {
.
.
/*
* Check for an Empty Element.
*/
if ((RAW == '/') && (NXT(1) == '>')) {
SKIP(2);
if (ctxt->sax2) {
if ((ctxt->sax != NULL) && (ctxt->sax->endElementNs != NULL) &&
(!ctxt->disableSAX))
ctxt->sax->endElementNs(ctxt->userData, name, prefix, URI);
.
.
}
xmlSAX2EndElementNs
함수는 요소의 끝이 감지되었을 때 콜백되며, 요소에 대한 네임스페이스 정보를 제공한다고 한다.
valid 옵션이 활성화되어 있으면 xmlValidateOneElement
함수를 호출한다.
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
/**
* xmlSAX2EndElementNs:
* @ctx: the user data (XML parser context)
* @localname: the local name of the element
* @prefix: the element namespace prefix if available
* @URI: the element namespace name if available
*
* SAX2 callback when an element end has been detected by the parser.
* It provides the namespace informations for the element.
*/
void
xmlSAX2EndElementNs(void *ctx,
const xmlChar * localname ATTRIBUTE_UNUSED,
const xmlChar * prefix ATTRIBUTE_UNUSED,
const xmlChar * URI ATTRIBUTE_UNUSED)
{
xmlParserCtxtPtr ctxt = (xmlParserCtxtPtr) ctx;
xmlParserNodeInfo node_info;
xmlNodePtr cur;
if (ctx == NULL) return;
cur = ctxt->node;
/* Capture end position and add node */
if ((ctxt->record_info) && (cur != NULL)) {
node_info.end_pos = ctxt->input->cur - ctxt->input->base;
node_info.end_line = ctxt->input->line;
node_info.node = cur;
xmlParserAddNodeInfo(ctxt, &node_info);
}
ctxt->nodemem = -1;
#ifdef LIBXML_VALID_ENABLED
if (ctxt->validate && ctxt->wellFormed &&
ctxt->myDoc && ctxt->myDoc->intSubset)
ctxt->valid &= xmlValidateOneElement(&ctxt->vctxt, ctxt->myDoc, cur);
#endif /* LIBXML_VALID_ENABLED */
/*
* end of parsing of this node.
*/
nodePop(ctxt);
}
xmlValidateOneElement
함수는 요소에 대해 유효성 검사를 진행하는 함수이다.
로직을 따라가다 보면 xmlValidateElementContent
함수를 호출한다.
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
/**
* xmlValidateOneElement:
* @ctxt: the validation context
* @doc: a document instance
* @elem: an element instance
*
* Try to validate a single element and it's attributes,
* basically it does the following checks as described by the
* XML-1.0 recommendation:
* - [ VC: Element Valid ]
* - [ VC: Required Attribute ]
* Then call xmlValidateOneAttribute() for each attribute present.
*
* The ID/IDREF checkings are done separately
*
* returns 1 if valid or 0 otherwise
*/
int
xmlValidateOneElement(xmlValidCtxtPtr ctxt, xmlDocPtr doc,
xmlNodePtr elem) {
.
.
child = elem->children;
cont = elemDecl->content;
tmp = xmlValidateElementContent(ctxt, child, elemDecl, 1, elem);
.
.
}
해당 함수는 요소의 콘텐츠를 검사한다.
1, 0, -1 중 하나를 리턴하며, 콘텐츠를 인자로 xmlSnprintfElementContent
함수를 호출한다.
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
/**
* xmlValidateElementContent:
* @ctxt: the validation context
* @child: the child list
* @elemDecl: pointer to the element declaration
* @warn: emit the error message
* @parent: the parent element (for error reporting)
*
* Try to validate the content model of an element
*
* returns 1 if valid or 0 if not and -1 in case of error
*/
static int
xmlValidateElementContent(xmlValidCtxtPtr ctxt, xmlNodePtr child,
xmlElementPtr elemDecl, int warn, xmlNodePtr parent) {
.
.
if ((warn) && ((ret != 1) && (ret != -3))) {
if (ctxt != NULL) {
char expr[5000];
char list[5000];
expr[0] = 0;
xmlSnprintfElementContent(&expr[0], 5000, cont, 1);
list[0] = 0;
.
.
}
해당 함수는 디버그 루틴을 위해 콘텐츠 정의의 콘텐츠를 덤프한다고 한다.
해당 함수에서 취약점이 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* xmlSnprintfElementContent:
* @buf: an output buffer
* @size: the buffer size
* @content: An element table
* @englob: 1 if one must print the englobing parenthesis, 0 otherwise
*
* This will dump the content of the element content definition
* Intended just for the debug routine
*/
void
xmlSnprintfElementContent(char *buf, int size, xmlElementContentPtr content, int englob) {
.
.
먼저, 크래시 파일은 다음과 같다.
다음과 같은 xml 구조를 DTD(문서 타입 정의) 구조라고 한다.
DTD 구조는 xml 문서의 구조를 정의함으로써 새로운 문서타입을 만들 수 있다.
또한 xml은 prefix를 정의할 수도 있다.
1
2
3
4
<!DOCTYPE a [
<!ELEMENT a (pppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppppp:llllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllllll)>
]>
<a/>
위의 xml 문서를 살펴보면 a가 루트 요소이며, 그 아래에 a라는 자식 요소가 있는 것을 알 수 있으며, ppp..
가 prefix이고 lll..
가 name인 것을 알 수 있다.
content
에는 해당 요소의 내용이 들어있다.
1
2
3
(gdb) p *(xmlElementContent*)0x604000000310
$9 = {type = XML_ELEMENT_CONTENT_ELEMENT, ocur = XML_ELEMENT_CONTENT_ONCE, name = 0x62a00000293b 'l' <repeats 200 times>..., c1 = 0x0, c2 = 0x0, parent = 0x1,
prefix = 0x62a00000199a 'p' <repeats 200 times>...}
해당 Element는 Element를 선언하고 있기 때문에 아래의 switch 문에서 XML_ELEMENT_CONTENT_ELEMENT
구문을 실행한다.
버퍼에 prefix
및 name
을 strcat
함수를 통해 붙여준다.
아래 로직에서 취약점이 발생한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
switch (content->type) {
case XML_ELEMENT_CONTENT_PCDATA:
strcat(buf, "#PCDATA");
break;
case XML_ELEMENT_CONTENT_ELEMENT:
if (content->prefix != NULL) {
if (size - len < xmlStrlen(content->prefix) + 10) {
strcat(buf, " ...");
return;
}
strcat(buf, (char *) content->prefix);
strcat(buf, ":");
}
if (size - len < xmlStrlen(content->name) + 10) {
strcat(buf, " ...");
return;
}
if (content->name != NULL)
strcat(buf, (char *) content->name);
break;
.
.
해당 시점에서 buf
의 사이즈는 5000이다.
해당 사이즈는 xmlValidateElementContent
함수에서 정의되었으며, 어떤 요소가 와도 버퍼의 사이즈는 항상 5000이다.
1
2
3
4
5
6
char expr[5000];
char list[5000];
expr[0] = 0;
xmlSnprintfElementContent(&expr[0], 5000, cont, 1);
list[0] = 0;
다시 XML_ELEMENT_CONTENT_ELEMENT
구문으로 돌아가서 prefix
+ name
의 크기를 계산해보면
6001개로 XML_ELEMENT_CONTENT_ELEMENT
구문의 조건문으로 인해 5000개 이후의 데이터는 추가되지 않는다.
사이즈 체크는 다음과 같이 계산된다.
여기서 len
변수는 해당 함수가 재귀적으로 호출되기 때문에 선언된 변수이며 버퍼의 문자열 길이를 가지고 있다.
1
2
3
4
if (size - len < xmlStrlen(content->name) + 10) {
strcat(buf, " ...");
return;
}
xmlStrlen
함수는 일반적인 strlen
함수의 구현과 동일하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/**
* xmlStrlen:
* @str: the xmlChar * array
*
* length of a xmlChar's string
*
* Returns the number of xmlChar contained in the ARRAY.
*/
int
xmlStrlen(const xmlChar *str) {
int len = 0;
if (str == NULL) return(0);
while (*str != 0) { /* non input consuming */
str++;
len++;
}
return(len);
}
해당 구문에서 prefix
와 name
의 사이즈를 따로 계산하여 버퍼에 이어붙이기 때문에 각 사이즈를 조건식을 통과하도록 만들어주면 stack-based buffer overflow
를 일으킬 수 있다. (CVE-2017-9047)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
case XML_ELEMENT_CONTENT_ELEMENT:
if (content->prefix != NULL) {
if (size - len < xmlStrlen(content->prefix) + 10) {
strcat(buf, " ...");
return;
}
strcat(buf, (char *) content->prefix);
strcat(buf, ":");
}
if (size - len < xmlStrlen(content->name) + 10) {
strcat(buf, " ...");
return;
}
if (content->name != NULL)
strcat(buf, (char *) content->name);
break;
또한 위의 switch 문이 종료되고, 아래의 구문이 실행된다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
if (englob)
strcat(buf, ")");
switch (content->ocur) {
case XML_ELEMENT_CONTENT_ONCE:
break;
case XML_ELEMENT_CONTENT_OPT:
strcat(buf, "?");
break;
case XML_ELEMENT_CONTENT_MULT:
strcat(buf, "*");
break;
case XML_ELEMENT_CONTENT_PLUS:
strcat(buf, "+");
break;
}
해당 로직에서는 버퍼에 대한 사이즈 검사가 없기 때문에 최대 두개의 문자를 더 이어붙일 수 있으므로, buffer overflow
를 일으킬 수 있다. (CVE-2017-9048)
취약점 패치
CVE-2017-9047
해당 취약점은 prefix
와 name
의 사이즈를 합쳐서 검사함으로써 buffer overflow
를 방지할 수 있도록 패치되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
case XML_ELEMENT_CONTENT_ELEMENT: {
int qnameLen = xmlStrlen(content->name);
if (content->prefix != NULL)
qnameLen += xmlStrlen(content->prefix) + 1;
if (size - len < qnameLen + 10) {
strcat(buf, " ...");
return;
}
if (content->prefix != NULL) {
strcat(buf, (char *) content->prefix);
strcat(buf, ":");
}
if (content->name != NULL)
strcat(buf, (char *) content->name);
break;
}
CVE-2017-9048
버퍼의 남은 사이즈가 2 이상이 아니라면 아무 동작 없이 리턴되도록 패치되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (size - strlen(buf) <= 2) return;
if (englob)
strcat(buf, ")");
switch (content->ocur) {
case XML_ELEMENT_CONTENT_ONCE:
break;
case XML_ELEMENT_CONTENT_OPT:
strcat(buf, "?");
break;
case XML_ELEMENT_CONTENT_MULT:
strcat(buf, "*");
break;
case XML_ELEMENT_CONTENT_PLUS:
strcat(buf, "+");
break;
}