Post

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++에서는 다양한 파일 포맷에 대한 딕셔너리를 제공해준다.

0


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 구문을 실행한다.

버퍼에 prefixnamestrcat 함수를 통해 붙여준다.

아래 로직에서 취약점이 발생한다.

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개 이후의 데이터는 추가되지 않는다.

3


사이즈 체크는 다음과 같이 계산된다.

여기서 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);
}


해당 구문에서 prefixname의 사이즈를 따로 계산하여 버퍼에 이어붙이기 때문에 각 사이즈를 조건식을 통과하도록 만들어주면 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

해당 취약점은 prefixname의 사이즈를 합쳐서 검사함으로써 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;
    }
This post is licensed under CC BY 4.0 by the author.