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
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 @@
1
- 슬레이브 인스턴스 실행
1
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 옵션으로 퍼저에게 딕셔너리를 제공할 수 있다.
- 커버리지를 넓히기 위해 다양한 프로그램 옵션을 주었다.
1개의 마스터 인스턴스와 3개의 슬레이브 인스턴스를 실행하였다.
크래시 분석
xmlSnprintfElementContent
함수에서 스택 버퍼 오버플로우가 발생하였다.
퍼저에게 주었던 여러 옵션 중에서 크래시가 터지는 옵션을 찾게 되어 해당 옵션을 주고 콜스택을 따라 분석을 진행하였다.
1
./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
/**
* 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
에는 해당 요소의 내용이 들어있다.
```(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>…}
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
<br>
해당 Element는 Element를 선언하고 있기 때문에 아래의 switch 문에서 `XML_ELEMENT_CONTENT_ELEMENT` 구문을 실행한다.
버퍼에 `prefix` 및 `name`을 `strcat` 함수를 통해 붙여준다.
아래 로직에서 취약점이 발생한다.
```c
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;
}