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->done
为True
,则会直接返回-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_code
与GetCode_
函数返回结果一致。
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
函数的地方,并且flag
为TRUE
。这里看到input_code_size
为c
。
gd_gif_in.c#L586
if (LWZReadByte(fd, &sd, TRUE, c, ZeroDataBlockP) < 0) {
return;
}
再追踪下c
从哪里来的,通过ReadOK
从fd
获取到的。其实也就是读取GIF图片里面一个字节。
gd_gif_in.c#L569
if (! ReadOK(fd,&c,1)) {
return;
}
前面使用ReadOK
函数读取GIF图片的一些信息,比如GIF89a
、高
、宽
之类的。到这里读取到是哪个字节?读取的是UBYTE LZWMinimumCodeSize
。如下图所示:
所以更改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
的结果。
还有(1 << (i % 8))
,这个值是1、2、4、8、16、32、64、128
的循环。综合这两点,是可以控制ret
值了。
比如:如果想返回结果为2
,code_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_size
为2
。
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的构造了
再看下官方给出的EXP
code_size
为4
,所以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)
跑出结果x
为136(0x88)
。图片里面也是用0x88
填充的。