最近GD库拒绝服务漏洞分析与EXP构造(CVE-2018-5711)

 

0x00 前言

最近爆出PHP GD库拒绝服务攻击漏洞,影响的版本比较多。官方上有漏洞的报告,但是看下来还是有不懂的地方,于是下载源码自己分析下。

 

0x01 漏洞分析

There is a do-while in file `ext/gd/libgd/gd_gif_in.c` and function `LWZReadByte_`
do {
    sd->firstcode = sd->oldcode =
    GetCode(fd, &sd->scd, sd->code_size, FALSE, ZeroDataBlockP);
} while (sd->firstcode == sd->clear_code);

https://github.com/php/php-src/blob/c5767db441e4db2a1e07b5880129ad7ce0b25b6f/ext/gd/libgd/gd_gif_in.c#L460


The implementation of `GetCode` is in `GetCode_`

static int
GetCode_(gdIOCtx *fd, CODE_STATIC_DATA *scd, int code_size, int flag, int *ZeroDataBlockP)
{
    int           i, j, ret;
    unsigned char count;

    ... 

    if ((count = GetDataBlock(fd, &scd->buf[2], ZeroDataBlockP)) <= 0)
        scd->done = TRUE;

    ...
}

https://github.com/php/php-src/blob/c5767db441e4db2a1e07b5880129ad7ce0b25b6f/ext/gd/libgd/gd_gif_in.c#L376

As you can see, `GetDataBlock` will read the image data and return the length. If EOF, returned -1. But the variable `count` is `unsigned char`, will always be positive value. So the line `scd->done = TRUE` will never be executed.

根据官方的报告,LWZReadByte_ 这个函数会造成死循环,原因是由于count变量是unsigne char,永远不会是负数,从而无法判断图片是否读取完毕,造成scd->done = TRUE无法执行,一开始没有想到这个报告很懒,还疑问那岂不是所有的GIF图片都会造成拒绝服务了(还真去拿普通的GIF图片试了试)。

其实还要满足sd->firstcode == sd->clear_code才能造成死循环。

do {
    sd->firstcode = sd->oldcode =
    GetCode(fd, &sd->scd, sd->code_size, FALSE, ZeroDataBlockP);
} while (sd->firstcode == sd->clear_code);

那为什么报告中要指出scd->done = TRUE无法执行。看这个函数上面,发现有一个if的判断,如果scd->doneTrue,则会直接返回-1。那么sd->firstcode == sd->clear_code永远不会成立了,造成循环退出。所以scd->done一定不能为True
gd_gif_in.c#L389

if ( (scd->curbit + code_size) >= scd->lastbit) {
    if (scd->done) {
        if (scd->curbit >= scd->lastbit) {
            /* Oh well */
        }
        return -1;
    }
    scd->buf[0] = scd->buf[scd->last_byte-2];
    scd->buf[1] = scd->buf[scd->last_byte-1];

    if ((count = GetDataBlock(fd, &scd->buf[2], ZeroDataBlockP)) <= 0)
        scd->done = TRUE;

    scd->last_byte = 2 + count;
    scd->curbit = (scd->curbit - scd->lastbit) + 16;
    scd->lastbit = (2+count)*8 ;
}

上面仅仅是为了满足不返回-1,但是还要满足返回结果等于sd->clear_code。接下来的ret结果由下面的代码控制。通过构造GIF,可以控制ret的返回结果。而sd->clear_code也是可以控制。从而达到死循环。
gd_gif_in.c#L407

if ((scd->curbit + code_size - 1) >= (CSD_BUF_SIZE * 8)) {
    ret = -1;
} else {
    ret = 0;
    for (i = scd->curbit, j = 0; j < code_size; ++i, ++j) {
        ret |= ((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j;
    }
}

scd->curbit += code_size;
return ret;

 

0x02 EXP构造

漏洞成因分析完了,知道EXP的关键点是控制sd->clear_codeGetCode_函数返回结果一致。

1.控制sd->clear_code

首先分下sd->clear_code是从哪里获取的。

获取函数的参数input_code_size,然后再把1左移input_code_size位。得到sd->clear_code
gd_gif_in.c#L431

static int
LWZReadByte_(gdIOCtx *fd, LZW_STATIC_DATA *sd, char flag, int input_code_size, int *ZeroDataBlockP)
{
    int code, incode, i;

    if (flag) {
        sd->set_code_size = input_code_size;
        sd->code_size = sd->set_code_size+1;
        sd->clear_code = 1 << sd->set_code_size ;
        sd->end_code = sd->clear_code + 1;
        sd->max_code_size = 2*sd->clear_code;
        sd->max_code = sd->clear_code+2;

再追踪下调用LWZReadByte函数的地方,并且flagTRUE。这里看到input_code_sizec
gd_gif_in.c#L586

if (LWZReadByte(fd, &sd, TRUE, c, ZeroDataBlockP) < 0) {
    return;
}

再追踪下c从哪里来的,通过ReadOKfd获取到的。其实也就是读取GIF图片里面一个字节。
gd_gif_in.c#L569

if (! ReadOK(fd,&c,1)) {
    return;
}

前面使用ReadOK函数读取GIF图片的一些信息,比如GIF89a之类的。到这里读取到是哪个字节?读取的是UBYTE LZWMinimumCodeSize。如下图所示:

1.png

所以更改UBYTE LZWMinimumCodeSize的值则可以控制sd->clear_code的值。

2.控制GetCode_返回结果ret

接下来就是控制GetCode_的返回结果ret,由如下代码控制。
gd_gif_in.c#L389

if ( (scd->curbit + code_size) >= scd->lastbit) {
    if (scd->done) {
        if (scd->curbit >= scd->lastbit) {
            /* Oh well */
        }
        return -1;
    }
    scd->buf[0] = scd->buf[scd->last_byte-2];
    scd->buf[1] = scd->buf[scd->last_byte-1];

    if ((count = GetDataBlock(fd, &scd->buf[2], ZeroDataBlockP)) <= 0)
        scd->done = TRUE;

    scd->last_byte = 2 + count;
    scd->curbit = (scd->curbit - scd->lastbit) + 16;
    scd->lastbit = (2+count)*8 ;
}

if ((scd->curbit + code_size - 1) >= (CSD_BUF_SIZE * 8)) {
    ret = -1;
} else {
    ret = 0;
    for (i = scd->curbit, j = 0; j < code_size; ++i, ++j) {
        ret |= ((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j;
    }
}

scd->curbit += code_size;
return ret;

最为关键的是如下代码。

for (i = scd->curbit, j = 0; j < code_size; ++i, ++j) {
    ret |= ((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j;
}

scd->buf是通过GetDataBlock获取到如下图data蓝色部分。内容全部为一样,因为可以使scd->buf[i / 8]保证获取到一个固定值。便于控制ret的结果。

2.png

还有(1 << (i % 8)),这个值是1、2、4、8、16、32、64、128的循环。综合这两点,是可以控制ret值了。

比如:如果想返回结果为2code_size控制为2的时候,再scd->buf[i/8]= 0xAA满足下面条件就可以返回2的结果。

scd->buf[i/8]&1==0 and scd->buf[i/8]&2!=0 and scd->buf[i/8]&4==0 and scd->buf[i/8]&8!=0 and scd->buf[i/8]&16==0 and scd->buf[i/8]&32!=0 and scd->buf[i/8]&64==0 and scd->buf[i/8]&128!=0

3.完整构造EXP过程

LZWMinimumCodeSize设置为1。那么sd->clear_code值为2。这个时候GetCode返回的值也必须是2

do {
    sd->firstcode = sd->oldcode =
        GetCode(fd, &sd->scd, sd->code_size, FALSE, ZeroDataBlockP);
} while (sd->firstcode == sd->clear_code);
return sd->firstcode;

此时code_size2

for (i = scd->curbit, j = 0; j < code_size; ++i, ++j) {
    ret |= ((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j;
}

意味着或运算两次。我们把第一次((scd->buf[i / 8] & (1 << (i % 8))) != 0) << j结果控制为0,第二次结果控制为2。这样0或2结果还是2。

ret=ret|((scd->buf[i /  8]  &  (1  <<  (i %  8)))  !=  0)  << j;
ret=ret|((scd->buf[i /  8]  &  (1  <<  (i %  8)))  !=  0)  << j;

那该怎么做尼?scd->buf[i/8]一直是固定值,(1 << (i % 8)))1、2、4、8、16、32、64、128的循环(至于为什么是这个循环,有点复杂,枯燥而且很长,由兴趣的可以自己下载php源码进行调试),写个python脚本遍历一下。寻找scd->buf[i / 8]为哪个固定值的时候,可以满足上面提出的条件。下面python脚本跑出结果x的值是170(0XAA)

for x in range(0,255):
    if(x&1==0 and x&2!=0 and x&4==0 and x&8!=0 and x&16==0 and x&32!=0 and x&64==0 and x&128!=0):
        print(x)

于是对正常的图片进行如下填充就完成EXP的构造了
3.png

再看下官方给出的EXP

code_size4,所以python脚本如下:

for x in range(0,255):
    if(x&1==0 and x&2==0 and x&4==0 and x&8!=0 and x&16==0 and x&32==0 and x&64==0 and x&128!=0):
        print(x)

跑出结果x136(0x88)。图片里面也是用0x88填充的。

 

0x03参考

https://bugs.php.net/bug.php?id=75571

(完)