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

  • 内容
  • 相关
鬼少EMLOG超强搬运工插件测试

 

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


转自安全客


鬼少EMLOG超强搬运工插件测试


本文由 搬运工 最近GD库拒绝服务漏洞分析与EXP构造(CVE-2018-5711) 摘抄,如有侵权请邮件告知。

本文标签:

版权声明:若无特殊注明,本文皆为《鬼少》原创,转载请保留文章出处。

本文链接:最近GD库拒绝服务漏洞分析与EXP构造(CVE-2018-5711) - http://tv1314.com/post-480.html

发表评论

电子邮件地址不会被公开。 必填项已用*标注

00:00 / 00:00
顺序播放