AFL Training Challenge 1 - libxml2
学一下 fuzzing. 暂时不是很想啃基础知识, 先来学学使用 AFL++. 找到个 afl-traning, 感觉挺新手友好的, 决定做它的 challenges. 给自己定的目标不是找到漏洞就完事, 而是尽量去分析甚至利用.
有个非常好用的工具 afl-utils, 集成了一些 fuzz 功能. 安装时需要切换到 experimental 分支.
编译
由于我装的 docker afl++, 其中 python 是 3.10, 直接编译 libxml2 会有问题, 需要打一下 这个 patch
首先用 afl-clang-fast
去编译, 设置一下编译器:
export CC=afl-clang-fast
export CXX=afl-clang-fast++
然后使用 libxml2 的 ./autogen.sh
生成 Makefile, 最后 make 编译. 编译时可以使用 ASAN 和 UBSAN, 去对地址和未定义行为监测, 使得之后的测试更快触发漏洞而崩溃.
这里直接用 AFL_USE_ASAN
和 AFL_USE_UBSAN
环境变量, 即可向编译选项中添加 -fsanitize
export AFL_USE_ASAN=1
export AFL_USE_UBSAN=1
./autogen.sh
make -j16
编译完后库文件在 .libs
下, 头文件在 include
中.
harness
libxml2 是用来解析 xml 的库, fuzz 的目标也是解析 xml 时的漏洞. 所以可以编写一个调用解析的 harness. 参考 libxml2 的 example, 比如 parse1.c 和 parse3.c.
很容易写出下面的 harness:
#include "libxml/parser.h"
#include "libxml/tree.h"
int main(int argc, char *argv[]) {
if (argc != 2)
return 0;
xmlDocPtr doc = xmlReadFile(argv[1], NULL, 0);
if (doc != NULL) {
xmlFreeDoc(doc);
}
xmlCleanupParser();
return 0;
}
编译然后开始 fuzzing:
afl-clang-fast ./harness.c -I libxml2/include libxml2/.libs/libxml2.a -lz -lm -o fuzzer
mkdir ./in
echo "<Wings>Hello World</Wings>" > in/a
afl-fuzz -i ./in -o ./out -- ./fuzzer @@
./in
中存放 seed 文件, 这里直接随便写一个符合格式的 xml. @@
会被替换成 seed 文件, 即 fuzzer 的参数是 seed 文件路径.
可以多核跑, afl-fuzz 加参数 -M name
指定主程序, -S name
指定从程序.
(然后开了四个线程跑了一个小时才跑出二十几个 crash)
有没有什么方法加速 fuzzing 呢? 答案是肯定的.
persistent mode
afl-clang 有一个叫 persistent 的模式, 阅读一下 官方的介绍, 大概可以知道是用一些小技巧来减少启动的开销, 从而缩短时间, 更专注于 fuzzing. 具体内容看文档即可, 不再赘述.
使用 persistent mode 编写 harness:
#include <unistd.h>
#include <libxml/parser.h>
#include <libxml/tree.h>
__AFL_FUZZ_INIT();
int main() {
#ifdef __AFL_HAVE_MANUAL_CONTROL
__AFL_INIT();
#endif
unsigned char *buf = __AFL_FUZZ_TESTCASE_BUF;
while (__AFL_LOOP(1000)) {
int len = __AFL_FUZZ_TESTCASE_LEN;
xmlDocPtr doc = xmlReadMemory(buf, len, "noname.xml", NULL, 0);
if (doc != NULL)
xmlFreeDoc(doc);
}
xmlCleanupParser();
return 0;
}
fuzzing
字典
但是对于 xml 解析器来说, 只有一些有意义的 xml 字段才会被解析, 而无意义的数据只会让程序结束. 在 fuzzing 时, 可以让 AFL 使用预设的字典, 从而生成更加结构化, 有意义的数据. ALF 仓库中就包含了一系列的字典, 在 ./dictionaries
文件夹下, 可以找到 xml.dict
.
afl-fuzz 用 -x path/to/dict
参数来指定字典.
输入命令启动 fuzz:
afl-fuzz -i in -o out -x /AFLplusplus/dictionaries/xml.dict ./fuzzer
多核
afl-utils 中的 afl-multicore 可以方便地使用多核. 对着文档写一个配置如下:
{
"fuzzer": "afl-fuzz",
"input": "./in",
"output": "./out",
"target": "./fuzzer",
"cmdline": "",
"dict": "/AFLplusplus/dictionaries/xml.dict",
"session": "SESSION",
"interactive": true,
"dirty": true,
"environment": []
}
然后在 screen 中启动 (就是想看 afl 的 ui):
afl-multicore -c ./libxml2-multicore.conf -s 1 start 8
-s
参数指定启动 afl-fuzz 的延迟, 如果不设置, 有些进程可能会启动失败 (不知道为什么).
下面是开八线程跑的结果:
用 persistent mode + 字典, 几分钟就能跑出第一个 crash
至少等跑了 1 个 cycle 再停止.
crash 整理
collect
afl-utils 中的 afl-collect 可以收集多线程运行的结果, 并用 gdb 插件 exploitable 分类 crash.
UBSAN_OPTIONS="abort_on_error=1:symbolize=0" ASAN_OPTIONS="abort_on_error=1:symbolize=0" afl-collect -d crashes.db -e gdb_script -r -rr ./out ./collection -j 16 -- ./fuzzer
由于我们使用 ASAN 和 UBSAN 来编译的程序, 有些地址越界什么的会被 san 返回 0, 从而被认为是没有 crash, 这里让 abort 返回非零值.
输出如下,
minimize
使用 afl-cmin 最小化输入数量, 有些输入能够覆盖的路径是一样的, 只需要保存一个就行. 可以减少样本.
使用 afl-tmin 最小化输入大小. 有些输入是冗余的, 删除部分也可以造成 crash, 所以可以将输入精简, 以便后续分析.
事实上这一步在建立语料库时也可以用. 假如在 fuzzing 前从某个地方找来了一些输入, 可以使用 afl-cmin 和 afl-tmin 缩减语料库, 以提高 fuzzing 的效率.
这里针对 collect 后的 crash 输入进行 afl-cmin 以及 afl-tmin, 可以将 300 多个输入减少到 77 个. 同时还发现有些输入是重复的, 再进行一次 afl-cmin, 就只有 20 个了.
afl-cmin -i in -o out -- ./fuzzer
afl-tmin 只能对一个输入文件进行, 所以写个脚本多线程 + 批量执行:
#!/bin/bash
cores=$1
inputdir=$2
outputdir=$3
target=$4
pids=""
total=`ls $inputdir | wc -l`
for k in `seq 1 $cores $total`
do
for i in `seq 0 $(expr $cores - 1)`
do
file=`ls -Sr $inputdir | sed $(expr $i + $k)"q;d"`
echo $file
afl-tmin -i $inputdir/$file -o $outputdir/$file -- $target &
done
wait
done
这个脚本没有考虑 harness 传参的情况.
afl-qtmin 8 ./in ./out ./fuzzer
crash exploration mode
(其实上面 20 个也完全能分析, 这里不过是继续体验一下 AFL 的其他功能)
afl 自身也有一个寻找可利用的 crash 输入的方法, 称为 crash exploration mode. 它可以将现有 crash 作为输入, 然后快速枚举程序中可以到达的所有代码路径, 找出不同路径的 crash. 使用方法也很简单, 只需要加 -C
参数, 然后把输入设置为 crashes 目录即可.
他能很快找出路径不同的 crash. 只不过执行速度比较慢, 普遍在 50/sec 以下, 还经常 zzzz. 跑到 10min 就都 zzzz 了…
15min 左右 master 跑了一个 cycle, 给他停了不跑了. 看了眼输入感觉长得和之前的也差不多qaq
将上述文件整合起来, 再跑一下 cmin 和 tmin, 一共得到 29 个 crashes.
分析与调试
总览一下数据, 一共可以分为三种.
第一种是 <?xml
开头, 加一个空白字符, 然后 encoding="
再跟几个字母 (某种 encoding), 最后跟一个不可见字符, 比如
00000000 3c 3f 78 6d 6c 0a 65 6e 63 6f 64 69 6e 67 3d 22 |<?xml.encoding="|
00000010 55 73 b8 |Us.|
00000013
第二种是 <?xml
前面还有几个字符, 至少有一个不可见字符, 然后之后的都是可见字符, 比如
00000000 ff fe 30 30 30 30 30 30 30 d9 3c 3f 78 6d 6c 20 |..0000000.<?xml |
00000010 20 20 30 30 30 30 30 30 30 30 30 30 30 30 30 30 | 00000000000000|
00000020 30 30 30 30 30 |00000|
00000025
ASAN 检测到和上面的一样的地方堆溢出.
第三种是带有 ":"
的内容, 比如:
00000000 3c 50 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |<P00000000000000|
00000010 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000|
*
00000250 30 30 30 30 30 30 30 30 30 30 30 30 3a 50 30 30 |000000000000:P00|
00000260 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000|
*
000003b0 30 30 30 30 30 30 30 30 30 30 |0000000000|
000003ba
CVE-2015-8317
第一类输入找到的就是这个 training 的目标, CVE-2015-8317. 所以先来分析这个. ASAN 检测如下:
根据 backtrace, 找到越界的代码 parse.c:
void
xmlParseXMLDecl(xmlParserCtxtPtr ctxt) {
// ...
if ((RAW == '?') && (NXT(1) == '>')) {
SKIP(2);
} else if (RAW == '>') {
/* Deprecated old WD ... */
xmlFatalErr(ctxt, XML_ERR_XMLDECL_NOT_FINISHED, NULL);
NEXT;
} else {
xmlFatalErr(ctxt, XML_ERR_XMLDECL_NOT_FINISHED, NULL);
MOVETO_ENDTAG(CUR_PTR);
NEXT;
}
}
这个函数处理 <?xml version="" encoding=""?>
这种声明. 当没有 ?>
闭合的时候, 就会走到 MOVETO_ENDTAG(CUR_PTR)
这一行. 这一行是个宏, 整个拆开是这样的:
while ((*ctxt->input->cur) && (*(ctxt->input->cur) != '>')) (ctxt->input->cur)++
ctxt
是解析用的结构体, 其中的 input
是处理所要解析的字符串. cur
是当前已经处理到的位置. 调试一下, 运行到这句话时, ctxt->input
长这样:
pwndbg> p *ctxt->input
$41 = {
buf = 0x606000000080,
filename = 0x602000000110 "noname.xml",
directory = 0x0,
base = 0x621000001500 "",
cur = 0x621000001512 '\276' <repeats 200 times>...,
end = 0x621000001500 "",
length = 0,
line = 1,
col = 19,
consumed = 0,
free = 0x0,
encoding = 0x6020000001b0 "Us",
version = 0x0,
standalone = -2,
id = 1
}
很明显, 这里的 cur
有问题, 它比 end
要大. 而且内容是一堆 \x276
(0xbe
). 那么这个 while 会一只找下去, 直到越界.
这里可能是 ASAN 填充了堆, 才导致它恰好能够一直 while 下去. 不开 ASAN 编译一个 libxml2 并调试, 到 while 这里时, input
长这样:
pwndbg> p *ctxt->input
$1 = {
buf = 0x558f4f2b0ec0,
filename = 0x558f4f2af890 "noname.xml",
directory = 0x0,
base = 0x558f4f2c49e0 "",
cur = 0x558f4f2c49f2 "",
end = 0x558f4f2c49e0 "",
length = 0,
line = 1,
col = 19,
consumed = 0,
free = 0x0,
encoding = 0x558f4f2b3560 "Us",
version = 0x0,
standalone = -2,
id = 1
}
虽然 cur
大于 end
, 但是 *cur = '\0'
, 所以并不会一直搜下去然后堆溢出.
单步调试发现, 这个 ctxt->input
是一个类似于缓冲区的东西, 其中 base
到 end
之间存的是需要解析的字符串, cur
是当前解析到哪个位置. 进入 xmlParseXMLDecl()
函数时, 缓冲区中是传入的输入.
xmlParseXMLDecl()
依次处理 "<?xml"
后跟的 version 和 encoding, 不过即使有格式上的错误也不会退出. 比如 fuzzing 出的这个输入就完全没有 version 字段, encoding 的引号也没有闭合.
是不是没有闭合引号导致的呢? 最后加一个引号上去, 甚至补完 "?>"
, 结果还是一样会 crash.
然而 NVD 的描述是
并且标题还是 heap oob, 于是乎找到了当年的 讨论, 一看好家伙确实是 ASAN 编译 AFL fuzzing 出来的, 然后就说这是 heap oob read. 但爷分析了一会貌似并不是这样, 或者说重点不是这里.
刚进入 xmlParseXMLDecl()
函数时, input
长这样:
在执行到下面这句话之前, 缓冲区还是原来的那个
void
xmlParseXMLDecl(xmlParserCtxtPtr ctxt) {
// ...
xmlParseEncodingDecl(ctxt);
// ...
}
而这个函数结束后, 缓冲区就变了. 很显然就是这里出了问题. 审计代码:
const xmlChar *
xmlParseEncodingDecl(xmlParserCtxtPtr ctxt) {
xmlChar *encoding = NULL;
SKIP_BLANKS;
if (CMP8(CUR_PTR, 'e', 'n', 'c', 'o', 'd', 'i', 'n', 'g')) {
SKIP(8);
SKIP_BLANKS;
if (RAW != '=') {
xmlFatalErr(ctxt, XML_ERR_EQUAL_REQUIRED, NULL);
return(NULL);
}
NEXT;
SKIP_BLANKS;
if (RAW == '"') {
NEXT;
encoding = xmlParseEncName(ctxt);
if (RAW != '"') {
xmlFatalErr(ctxt, XML_ERR_STRING_NOT_CLOSED, NULL);
} else
NEXT;
} else if (RAW == '\''){
NEXT;
encoding = xmlParseEncName(ctxt);
if (RAW != '\'') {
xmlFatalErr(ctxt, XML_ERR_STRING_NOT_CLOSED, NULL);
} else
NEXT;
} else {
xmlFatalErr(ctxt, XML_ERR_STRING_NOT_STARTED, NULL);
}
// ...
}
这一部分是在解析 encoding="
或者 encoding='
, 找到编码的名字, 然后进入 xmlParseEncName()
继续解析.
/**
* xmlParseEncName:
* @ctxt: an XML parser context
*
* parse the XML encoding name
*
* [81] EncName ::= [A-Za-z] ([A-Za-z0-9._] | '-')*
*
* Returns the encoding name value or NULL
*/
xmlChar *
xmlParseEncName(xmlParserCtxtPtr ctxt) {
xmlChar *buf = NULL;
int len = 0;
int size = 10;
xmlChar cur;
cur = CUR;
if (((cur >= 'a') && (cur <= 'z')) ||
((cur >= 'A') && (cur <= 'Z'))) {
buf = (xmlChar *) xmlMallocAtomic(size * sizeof(xmlChar));
if (buf == NULL) {
xmlErrMemory(ctxt, NULL);
return(NULL);
}
buf[len++] = cur;
NEXT;
cur = CUR;
while (((cur >= 'a') && (cur <= 'z')) ||
((cur >= 'A') && (cur <= 'Z')) ||
((cur >= '0') && (cur <= '9')) ||
(cur == '.') || (cur == '_') ||
(cur == '-')) {
if (len + 1 >= size) {
xmlChar *tmp;
size *= 2;
tmp = (xmlChar *) xmlRealloc(buf, size * sizeof(xmlChar));
if (tmp == NULL) {
xmlErrMemory(ctxt, NULL);
xmlFree(buf);
return(NULL);
}
buf = tmp;
}
buf[len++] = cur;
NEXT;
cur = CUR;
if (cur == 0) {
SHRINK;
GROW;
cur = CUR;
}
}
buf[len] = 0;
} else {
xmlFatalErr(ctxt, XML_ERR_ENCODING_NAME, NULL);
}
return(buf);
}
这里再解析编码的名字, 与其说是名字, 不如说是正则匹配. 匹配失败后返回匹配到的字符串, 也就是 encoding.
const xmlChar *
xmlParseEncodingDecl(xmlParserCtxtPtr ctxt) {
// ...
/*
* UTF-16 encoding stwich has already taken place at this stage,
* more over the little-endian/big-endian selection is already done
*/
if ((encoding != NULL) &&
((!xmlStrcasecmp(encoding, BAD_CAST "UTF-16")) ||
(!xmlStrcasecmp(encoding, BAD_CAST "UTF16")))) {
/*
* If no encoding was passed to the parser, that we are
* using UTF-16 and no decoder is present i.e. the
* document is apparently UTF-8 compatible, then raise an
* encoding mismatch fatal error
*/
if ((ctxt->encoding == NULL) &&
(ctxt->input->buf != NULL) &&
(ctxt->input->buf->encoder == NULL)) {
xmlFatalErrMsg(ctxt, XML_ERR_INVALID_ENCODING,
"Document labelled UTF-16 but has UTF-8 content\n");
}
if (ctxt->encoding != NULL)
xmlFree((xmlChar *) ctxt->encoding);
ctxt->encoding = encoding;
}
/*
* UTF-8 encoding is handled natively
*/
else if ((encoding != NULL) &&
((!xmlStrcasecmp(encoding, BAD_CAST "UTF-8")) ||
(!xmlStrcasecmp(encoding, BAD_CAST "UTF8")))) {
if (ctxt->encoding != NULL)
xmlFree((xmlChar *) ctxt->encoding);
ctxt->encoding = encoding;
}
else if (encoding != NULL) {
xmlCharEncodingHandlerPtr handler;
if (ctxt->input->encoding != NULL)
xmlFree((xmlChar *) ctxt->input->encoding);
ctxt->input->encoding = encoding;
handler = xmlFindCharEncodingHandler((const char *) encoding);
if (handler != NULL) {
xmlSwitchToEncoding(ctxt, handler);
} else {
xmlFatalErrMsgStr(ctxt, XML_ERR_UNSUPPORTED_ENCODING,
"Unsupported encoding %s\n", encoding);
return(NULL);
}
}
// ...
return(encoding);
}
之后判断一下 UTF16 和 UTF8 的两个特例, 不是这两个的话进入最后一个 if 语句, 执行 xmlSwitchToEncoding()
进行编码转换.
int
xmlSwitchToEncoding(xmlParserCtxtPtr ctxt, xmlCharEncodingHandlerPtr handler)
{
return (xmlSwitchToEncodingInt(ctxt, handler, -1));
}
static int
xmlSwitchToEncodingInt(xmlParserCtxtPtr ctxt,
xmlCharEncodingHandlerPtr handler, int len) {
int ret = 0;
if (handler != NULL) {
if (ctxt->input != NULL) {
ret = xmlSwitchInputEncodingInt(ctxt, ctxt->input, handler, len);
} else {
xmlErrInternal(ctxt, "xmlSwitchToEncoding : no input\n",
NULL);
return(-1);
}
/*
* The parsing is now done in UTF8 natively
*/
ctxt->charset = XML_CHAR_ENCODING_UTF8;
} else
return(-1);
return(ret);
}
经过一些封装, 到 xmlSwitchInputEncodingInt()
函数. 其中 input
参数是 ctxt->input
缓冲区.
static int
xmlSwitchInputEncodingInt(xmlParserCtxtPtr ctxt, xmlParserInputPtr input,
xmlCharEncodingHandlerPtr handler, int len)
{
int nbchars;
if (handler == NULL)
return (-1);
if (input == NULL)
return (-1);
if (input->buf != NULL) {
if (input->buf->encoder != NULL) {
/*
* Check in case the auto encoding detetection triggered
* in already.
*/
if (input->buf->encoder == handler)
return (0);
/*
* "UTF-16" can be used for both LE and BE
if ((!xmlStrncmp(BAD_CAST input->buf->encoder->name,
BAD_CAST "UTF-16", 6)) &&
(!xmlStrncmp(BAD_CAST handler->name,
BAD_CAST "UTF-16", 6))) {
return(0);
}
*/
/*
* Note: this is a bit dangerous, but that's what it
* takes to use nearly compatible signature for different
* encodings.
*/
xmlCharEncCloseFunc(input->buf->encoder);
input->buf->encoder = handler;
return (0);
}
input->buf->encoder = handler;
// ...
首先检查是否已经有 encoder 了 (就是编码器函数指针, 负责编码), 没有的话就给他赋值为新传进来的这个 handler. 这里是第一次进行编码, 所以 encoder == NULL
.
/*
* Is there already some content down the pipe to convert ?
*/
if (xmlBufIsEmpty(input->buf->buffer) == 0) {
int processed;
unsigned int use;
/*
* Specific handling of the Byte Order Mark for
* UTF-16
*/
if ((handler->name != NULL) &&
(!strcmp(handler->name, "UTF-16LE") ||
!strcmp(handler->name, "UTF-16")) &&
(input->cur[0] == 0xFF) && (input->cur[1] == 0xFE)) {
input->cur += 2;
}
if ((handler->name != NULL) &&
(!strcmp(handler->name, "UTF-16BE")) &&
(input->cur[0] == 0xFE) && (input->cur[1] == 0xFF)) {
input->cur += 2;
}
/*
* Errata on XML-1.0 June 20 2001
* Specific handling of the Byte Order Mark for
* UTF-8
*/
if ((handler->name != NULL) &&
(!strcmp(handler->name, "UTF-8")) &&
(input->cur[0] == 0xEF) &&
(input->cur[1] == 0xBB) && (input->cur[2] == 0xBF)) {
input->cur += 3;
}
/*
* Shrink the current input buffer.
* Move it as the raw buffer and create a new input buffer
*/
processed = input->cur - input->base;
xmlBufShrink(input->buf->buffer, processed);
input->buf->raw = input->buf->buffer;
input->buf->buffer = xmlBufCreate();
input->buf->rawconsumed = processed;
use = xmlBufUse(input->buf->raw);
然后判断缓冲区中是否还有输入, 有的话得处理这里面的编码先. 前面一部分是对 UTF16 和 UTF8 的特判, 可以不管他, 主要看后面一部分. 这里用 xmlBufShrink()
将缓冲区调整到未处理的地方, 然后让 raw
指向原来的 buffer, 用 xmlBufCreate()
申请新的内存来存放编码后的内容. 跟进一下 xmlBufCreate()
:
/**
* xmlBufCreate:
*
* routine to create an XML buffer.
* returns the new structure.
*/
xmlBufPtr
xmlBufCreate(void) {
xmlBufPtr ret;
ret = (xmlBufPtr) xmlMalloc(sizeof(xmlBuf));
if (ret == NULL) {
xmlBufMemoryError(NULL, "creating buffer");
return(NULL);
}
ret->compat_use = 0;
ret->use = 0;
ret->error = 0;
ret->buffer = NULL;
ret->size = xmlDefaultBufferSize;
ret->compat_size = xmlDefaultBufferSize;
ret->alloc = xmlBufferAllocScheme;
ret->content = (xmlChar *) xmlMallocAtomic(ret->size * sizeof(xmlChar));
if (ret->content == NULL) {
xmlBufMemoryError(ret, "creating buffer");
xmlFree(ret);
return(NULL);
}
ret->content[0] = 0;
ret->contentIO = NULL;
return(ret);
}
buffer 的 content
是申请分配了 4096 (xmlDefaultBufferSize
) 个字节的堆块指针. 这个 xmlMallocAtomic()
是对 malloc()
的一层封装.
是的, 就是这里导致缓冲区重新分配了. 继续看后面的处理:
// ...
if (ctxt->html) {
/*
* convert as much as possible of the buffer
*/
nbchars = xmlCharEncInput(input->buf, 1);
} else {
/*
* convert just enough to get
* '<?xml version="1.0" encoding="xxx"?>'
* parsed with the autodetected encoding
* into the parser reading buffer.
*/
nbchars = xmlCharEncFirstLineInput(input->buf, len);
}
if (nbchars < 0) {
xmlErrInternal(ctxt,
"switching encoding: encoder error\n",
NULL);
return (-1);
}
input->buf->rawconsumed += use - xmlBufUse(input->buf->raw);
xmlBufResetInput(input->buf->buffer, input);
}
return (0);
} else if (input->length == 0) {
/*
* When parsing a static memory array one must know the
* size to be able to convert the buffer.
*/
xmlErrInternal(ctxt, "switching encoding : no input\n", NULL);
return (-1);
}
return (0);
}
调试一下 ctxt->html
是 0, 所以会进入 else 分支. 根据注释, 是要用新的编码解析第一行 — xml 定义语句. 之后是对缓冲区的调整. xmlCharEncFirstLineInput()
如下:
int
xmlCharEncFirstLineInput(xmlParserInputBufferPtr input, int len)
{
int ret = -2;
size_t written;
size_t toconv;
int c_in;
int c_out;
xmlBufPtr in;
xmlBufPtr out;
if ((input == NULL) || (input->encoder == NULL) ||
(input->buffer == NULL) || (input->raw == NULL))
return (-1);
out = input->buffer;
in = input->raw;
toconv = xmlBufUse(in);
if (toconv == 0)
return (0);
written = xmlBufAvail(out) - 1; /* count '\0' */
/*
* echo '<?xml version="1.0" encoding="UCS4"?>' | wc -c => 38
* 45 chars should be sufficient to reach the end of the encoding
* declaration without going too far inside the document content.
* on UTF-16 this means 90bytes, on UCS4 this means 180
* The actual value depending on guessed encoding is passed as @len
* if provided
*/
if (len >= 0) {
if (toconv > (unsigned int) len)
toconv = len;
} else {
if (toconv > 180)
toconv = 180;
}
if (toconv * 2 >= written) {
xmlBufGrow(out, toconv * 2);
written = xmlBufAvail(out) - 1;
}
if (written > 360)
written = 360;
c_in = toconv;
c_out = written;
if (input->encoder->input != NULL) {
ret = input->encoder->input(xmlBufEnd(out), &c_out,
xmlBufContent(in), &c_in);
xmlBufShrink(in, c_in);
xmlBufAddLen(out, c_out);
}
#ifdef LIBXML_ICONV_ENABLED
else if (input->encoder->iconv_in != NULL) {
ret = xmlIconvWrapper(input->encoder->iconv_in, xmlBufEnd(out),
&c_out, xmlBufContent(in), &c_in);
xmlBufShrink(in, c_in);
xmlBufAddLen(out, c_out);
if (ret == -1)
ret = -3;
}
#endif /* LIBXML_ICONV_ENABLED */
#ifdef LIBXML_ICU_ENABLED
else if (input->encoder->uconv_in != NULL) {
ret = xmlUconvWrapper(input->encoder->uconv_in, 1, xmlBufEnd(out),
&c_out, xmlBufContent(in), &c_in);
xmlBufShrink(in, c_in);
xmlBufAddLen(out, c_out);
if (ret == -1)
ret = -3;
}
#endif /* LIBXML_ICU_ENABLED */
switch (ret) {
case 0:
#ifdef DEBUG_ENCODING
xmlGenericError(xmlGenericErrorContext,
"converted %d bytes to %d bytes of input\n",
c_in, c_out);
#endif
break;
case -1:
#ifdef DEBUG_ENCODING
xmlGenericError(xmlGenericErrorContext,
"converted %d bytes to %d bytes of input, %d left\n",
c_in, c_out, (int)xmlBufUse(in));
#endif
break;
case -3:
#ifdef DEBUG_ENCODING
xmlGenericError(xmlGenericErrorContext,
"converted %d bytes to %d bytes of input, %d left\n",
c_in, c_out, (int)xmlBufUse(in));
#endif
break;
case -2: {
char buf[50];
const xmlChar *content = xmlBufContent(in);
snprintf(&buf[0], 49, "0x%02X 0x%02X 0x%02X 0x%02X",
content[0], content[1],
content[2], content[3]);
buf[49] = 0;
xmlEncodingErr(XML_I18N_CONV_FAILED,
"input conversion failed due to input error, bytes %s\n",
buf);
}
}
/*
* Ignore when input buffer is not on a boundary
*/
if (ret == -3) ret = 0;
if (ret == -1) ret = 0;
return(ret);
}
大致看一下就是根据 encoder
, 将 input->raw
编码输出到 input->buffer
. 调试一下, 如果将要转换的字符不符合编码的规则, 则 content
不会有任何字符串被拷贝或者编码上去. 否则 content
是编码之后的字符串.
回到 xmlParseXMLDecl()
void
xmlParseXMLDecl(xmlParserCtxtPtr ctxt) {
// ...
xmlParseEncodingDecl(ctxt);
if (ctxt->errNo == XML_ERR_UNSUPPORTED_ENCODING) {
/*
* The XML REC instructs us to stop parsing right here
*/
return;
}
/*
* We may have the standalone status.
*/
if ((ctxt->input->encoding != NULL) && (!IS_BLANK_CH(RAW))) {
if ((RAW == '?') && (NXT(1) == '>')) {
SKIP(2);
return;
}
xmlFatalErrMsg(ctxt, XML_ERR_SPACE_REQUIRED, "Blank needed here\n");
}
/*
* We can grow the input buffer freely at that point
*/
GROW;
// ...
}
这里虽然编码失败, 但是不是 unsupported encoding, 也没有跟 ?>
, 而是一个不可见字符, 所以这里还不会退出, 而是执行下的 GROW
宏:
#define GROW if ((ctxt->progressive == 0) && \
(ctxt->input->end - ctxt->input->cur < INPUT_CHUNK)) \
xmlGROW (ctxt);
static void xmlGROW (xmlParserCtxtPtr ctxt) {
unsigned long curEnd = ctxt->input->end - ctxt->input->cur;
unsigned long curBase = ctxt->input->cur - ctxt->input->base;
if (((curEnd > (unsigned long) XML_MAX_LOOKUP_LIMIT) ||
(curBase > (unsigned long) XML_MAX_LOOKUP_LIMIT)) &&
((ctxt->input->buf) && (ctxt->input->buf->readcallback != (xmlInputReadCallback) xmlNop)) &&
((ctxt->options & XML_PARSE_HUGE) == 0)) {
xmlFatalErr(ctxt, XML_ERR_INTERNAL_ERROR, "Huge input lookup");
ctxt->instate = XML_PARSER_EOF;
}
xmlParserInputGrow(ctxt->input, INPUT_CHUNK);
if ((ctxt->input->cur != NULL) && (*ctxt->input->cur == 0) &&
(xmlParserInputGrow(ctxt->input, INPUT_CHUNK) <= 0))
xmlPopInput(ctxt);
}
执行到下面的 xmlParserInputGrow()
, 这个函数前半部分的检查都能通过, 直接看最最关键的部分:
int
xmlParserInputGrow(xmlParserInputPtr in, int len) {
// ...
content = xmlBufContent(in->buf->buffer);
if (in->base != content) {
/*
* the buffer has been reallocated
*/
indx = in->cur - in->base;
in->base = content;
in->cur = &content[indx];
}
in->end = xmlBufEnd(in->buf->buffer);
CHECK_BUFFER(in);
return(ret);
}
就是这个地方, 将 ctxt->input
的缓冲区换到了之前申请的 ctxt->input->buf->buffer->content
上, 同时处理了 cur
和 end
. 而这个 cur
是拿之前的 cur - base 算的, end
却是新申请的 buffer 算的:
xmlChar *
xmlBufEnd(xmlBufPtr buf)
{
if ((!buf) || (buf->error))
return NULL;
CHECK_COMPAT(buf)
return(&buf->content[buf->use]);
}
这里 buffer 因为处理不了输入的奇怪字符, 所以缓冲区中没有东西, use
= 0. 所以实际上的结果就是 base
= end
= &content[0]
, 而 cur
> end
, 也就是之前看到的奇怪现象.
所以这个 GROW 将新申请的 buffer content 换到了 input 缓冲区, 但是这个 content 其实是错误的, 它除了最分配时写的一个 content[0] = '\0'
外, 其余的数据都是堆上残留的. 而这里的 cur
指针又是根据先前解析的字符来计算的, 所以后续的 MOVETO_ENDTAG
越界访问了堆数据.
这个越界读可能泄漏地址啥的, 得撞一个 “<”, 然后编码错误, 会输出后续 4 个字符. 如果是在没有 safe link 的 glibc 版本下, 应该能风水一下, $\frac{1}{16}$ 撞一个 tcache->next
的倒数第二字节为 “<”, 泄漏堆地址. 其他情况暂时没有想到.
第二类长这样的 \xff000<xml 000
数据貌似还和输入的长度有关, 感觉也是哪儿去给缓冲区分配了一下, 导致溢出读了. 分析不动了累了. 咕咕咕.
CVE-2015-7497
最后一类输入长这样:
00000000 3c 50 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |<P00000000000000|
00000010 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000|
*
00000250 30 30 30 30 30 30 30 30 30 30 30 30 3a 50 30 30 |000000000000:P00|
00000260 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 30 |0000000000000000|
*
000003b0 30 30 30 30 30 30 30 30 30 30 |0000000000|
000003ba
ASAN 检测如下:
看一下出问题的地方:
/*
* xmlDictComputeFastQKey:
*
* Calculate a hash key for two strings using a fast hash function
* that works well for low hash table fill.
*
* Neither of the two strings must be NULL.
*/
static unsigned long
xmlDictComputeFastQKey(const xmlChar *prefix, int plen,
const xmlChar *name, int len, int seed)
{
unsigned long value = (unsigned long) seed;
if (plen == 0)
value += 30 * (unsigned long) ':';
else
value += 30 * (*prefix);
if (len > 10) {
value += name[len - (plen + 1 + 1)];
len = 10;
if (plen > 10)
plen = 10;
}
// ...
}
明显越界. 看一下注释是在算哈希, backtrace 中看到调用它的函数为 xmlDictQLookup()
:
/**
* xmlDictQLookup:
* @dict: the dictionnary
* @prefix: the prefix
* @name: the name
*
* Add the QName @prefix:@name to the hash @dict if not present.
*
* Returns the internal copy of the QName or NULL in case of internal error
*/
const xmlChar *
xmlDictQLookup(xmlDictPtr dict, const xmlChar *prefix, const xmlChar *name) {
unsigned long okey, key, nbi = 0;
xmlDictEntryPtr entry;
xmlDictEntryPtr insert;
const xmlChar *ret;
unsigned int len, plen, l;
if ((dict == NULL) || (name == NULL))
return(NULL);
if (prefix == NULL)
return(xmlDictLookup(dict, name, -1));
l = len = strlen((const char *) name);
plen = strlen((const char *) prefix);
len += 1 + plen;
/*
* Check for duplicate and insertion location.
*/
okey = xmlDictComputeQKey(dict, prefix, plen, name, l);
key = okey % dict->size;
if (dict->dict[key].valid == 0) {
insert = NULL;
} else {
for (insert = &(dict->dict[key]); insert->next != NULL;
insert = insert->next) {
if ((insert->okey == okey) && (insert->len == len) &&
(xmlStrQEqual(prefix, name, insert->name)))
return(insert->name);
nbi++;
}
if ((insert->okey == okey) && (insert->len == len) &&
(xmlStrQEqual(prefix, name, insert->name)))
return(insert->name);
}
// ...
}
看注释大概是有个 QName 的东西, 由 prefix 和 name 中间加冒号组成, 然后算哈希, 用作存入字典的 key. 这里就是为什么输入有 :
的原因.
触发堆溢出的这句话 value += name[len - (plen + 1 + 1)];
完全没有检查, 不过也只能算一个哈希, 甚至泄漏地址都很难做到. 但是看了一圈下来, 说是能造成程序崩溃然后拒绝服务. 想想也是, 啥也不检查的话直接给他溢出到非法地址, 程序就寄了. 虽然我拿不到 shell, 但是你们也别想正常使用