Windows图像解析Fuzzing(一):颜色配置文件

2020-10-20

一、概述

图像解析和渲染是任何现代操作系统(OS)的基础功能。同时,图像解析也是一个易于访问的攻击面,这一功能可以导致远程代码执行或信息泄露风险,对攻击者来说非常有价值。在这一系列文章中,我将回顾Windows操作系统内置的图像解析器和相关文件格式,特别关注构建工具、寻找语料库(Corpus)以及通过模糊测试(Fuzzing)发现漏洞的过程。在第一部分中,我们重点分析颜色配置文件。该文件并不是图像格式本身,而是规则嵌入在图像中的颜色。

二、ICC颜色配置文件分析

在维基百科上,提供了对ICC颜色配置文件的定义:“在颜色管理过程中,根据国际颜色协会(ICC)发布的标准,ICC配置文件是一组定义颜色输入输出设备以及颜色空间的数据。配置文件通过定义设备源或目标颜色空间与配置文件连接空间(PCS)之间的映射,描述特定设备的颜色属性或预览要求。这里的PCS可以是CIELAB (L*a*b*),或者是CIEXYZ。可以使用定义插值(Interpolation)的表格来指定映射,也可以通过一系列参数进行转换。

简而言之,ICC颜色配置文件是一个二进制文件,该文件会嵌入到图像中,并在ICC支持的软件处理图像时进行解析。

三、ICC规范

ICC规范大概有100页,相对比较容易浏览。通读规范可以更好地理解文件格式,不同类型的颜色配置文件以及颜色转换背后的数学原理。此外,了解文件格式的内部结构可以为我们提供信息,以便更好地优化模糊测试、选择良好的语料库以及准备模糊测试的字典。

四、关于Windows颜色管理

从Windows 95开始,发布了图像颜色管理(ICM)的1.0版本,从Windows 98以后开始发布2.0版本。从Windows Vista开始,对Windows色彩系统(WCS)1.0版本进行了大幅改动。ICC颜色配置文件是二进制文件,而WCS颜色配置文件则使用XML作为其文件格式。在这篇文章中功能,我们专注于分析ICC颜色配置文件。

Microsoft曾发布过支持的Windows API的列表,其中就包含一些名称非常明显的API,例如OpenColorProfile,我们可以看到它是在MSCMS.dll中实现的。这个DLL是通用入口点,支持加载Microsoft的颜色管理模块(CMM)和第三方颜色管理模块(例如Adobe的CMM)。Microsoft的CMM(即ICM)可以在system32目录中找到,名称为ICM32.dll。

ICM32:

Windows图像解析Fuzzing(一):颜色配置文件

Windows的CMM是在Windows 95时代由第三方编写的,发展至今仍然或多或少地包含当时的代码,但已经经过了数十年来的安全修复。既然是如此古老的模块,我们就有希望在其中发现新的漏洞。但是,这也是一个非常小的模块,可能经过了多轮审计和模糊测试,包括内部产品安全团队和外部研究人员,这个事实在一定程度上降低了我们找到漏洞的希望。我们检索近期发现的ICM32漏洞,可以发现Project Zero和ZDI的研究人员在2017-2018年期间发现了多个漏洞,但从2019年开始就没有找到更多的研究成果。

五、构建工具

尽管在MSDN上有ICM API的列表,但我们需要找到Windows用于所有ICC相关操作的API序列。要查找API序列,一种方式是搜索Windows DLL和EXE的反汇编并寻找用到的颜色配置文件API,另一种方式是找到适用于开源色彩管理系统(例如Little CMS,LCMS)的工具。使用这两种方式,最终共同指向了很少的几个API,这些API具有打开颜色配置文件和创建颜色转换的功能。

基于上述信息,我编写了一个简单的初始工具:

  1. #include < stdio.h > 
  2. #include < Windows.h > 
  3. #include < Icm.h > 
  4. #pragma comment(lib, "mscms.lib"
  5. int main(int argc, char** argv) 
  6.     char dstProfilePath[] = "sRGB Color Space Profile.icm"
  7.     tagPROFILE destinationProfile; 
  8.     HPROFILE   hDstProfile = nullptr;   
  9.     destinationProfile.dwType = PROFILE_FILENAME; 
  10.     destinationProfile.pProfileData = dstProfilePath; 
  11.     destinationProfile.cbDataSize = (strlen(dstProfilePath) + 1); 
  12.     hDstProfile = OpenColorProfileA(&destinationProfile, PROFILE_READ, 
  13.         FILE_SHARE_READ, OPEN_EXISTING); 
  14.     if (nullptr == hDstProfile) 
  15.     { 
  16.         return -1; 
  17.     }   
  18.     tagPROFILE sourceProfile; 
  19.     HPROFILE   hSrcProfile = nullptr; 
  20.     HTRANSFORM hColorTransform = nullptr;     
  21.     DWORD dwIntent[] = { INTENT_PERCEPTUAL, INTENT_PERCEPTUAL }; 
  22.     HPROFILE hProfileList[2];   
  23.     sourceProfile.dwType = PROFILE_FILENAME; 
  24.     sourceProfile.pProfileData = argv[1]; 
  25.     sourceProfile.cbDataSize = (strlen(argv[1]) + 1); 
  26.     hSrcProfile = OpenColorProfileA(&sourceProfile, PROFILE_READ, 
  27.         FILE_SHARE_READ, OPEN_EXISTING); 
  28.     if (nullptr == hSrcProfile) 
  29.     { 
  30.         return -1; 
  31.     }   
  32.     hProfileList[0] = hSrcProfile; 
  33.     hProfileList[1] = hDstProfile; 
  34.     hColorTransform = CreateMultiProfileTransform( 
  35.         hProfileList, 
  36.         2, 
  37.         dwIntent, 
  38.         2, 
  39.         USE_RELATIVE_COLORIMETRIC | BEST_MODE, 
  40.         INDEX_DONT_CARE 
  41.     ); 
  42.     if (nullptr == hColorTransform) 
  43.     { 
  44.         return -1; 
  45.     }   
  46.     DeleteColorTransform(hColorTransform); 
  47.     CloseColorProfile(hSrcProfile); 
  48.     CloseColorProfile(hDstProfile); 
  49.     return 0; 

六、寻找语料库和字典

在互联网上,可以找到大量提供颜色配置文件的网站。颜色配置文件的另一个主要来源是图像。有许多图像文件都包含颜色配置文件,但需要一些工具将颜色配置文件转储到单独的文件中。

简单浏览规范,我们还可以保证语料库至少包含来自7个不同颜色配置文件的所有样本。而将它与代码覆盖率信息结合,可以准备第一套语料,用于模糊测试。

我们可以在梳理规范的过程中,创建唯一标签名称和对应值的列表,从而准备字典,帮助模糊工具查找其他代码路径。另外,还可以从LCMS这样的开源模糊测试尝试过程中找到字典。

七、模糊测试

我使用16核主机对第一套语料进行模糊处理,同时将来自MSCMS.dll和ICM32.dll的代码覆盖率信息作为我的模糊工具的反馈。在几天后,开始出现崩溃。

八、CVE-2020-1117:InitNamedColorProfileData中的堆溢出

在尝试越界读取时,icm32!SwapShortOffset中发生了以下崩溃:

  1. 0:000 > r 
  2. rax=0000023690497000 rbx=0000000000000000 rcx=00000000000000ff 
  3. rdx=000000000000ffff rsi=0000023690496f00 rdi=0000023690496fee 
  4. rip=00007ffa46bf3790 rsp=000000c2a56ff5a8 rbp=0000000000000001 
  5.  r8=0000000000000014  r9=0000023690497002 r10=0000000000000014 
  6. r11=0000000000000014 r12=000000c2a56ff688 r13=0000023690492de0 
  7. r14=000000000000000a r15=000000004c616220 
  8. iopl=0         nv up ei ng nz ac pe cy 
  9. cs=0033  ss=002b  ds=002b  es=002b  fs=0053  gs=002b             efl=00000293 
  10. icm32!SwapShortOffset+0x10: 
  11. 00007ffa`46bf3790 0fb610          movzx   edx,byte ptr [rax] ds:00000236`90497000=?? 
  12.   
  13. 0:000 > !heap -p -a @rax 
  14.     address 0000023690497000 found in 
  15.     _DPH_HEAP_ROOT @ 23690411000 
  16.     in busy allocation (  DPH_HEAP_BLOCK:         UserAddr         UserSize -         VirtAddr         VirtSize) 
  17.                              23690412b60:      23690496f00              100 -      23690496000             2000 
  18.     00007ffa51644807 ntdll!RtlDebugAllocateHeap+0x000000000000003f 
  19.     00007ffa515f49d6 ntdll!RtlpAllocateHeap+0x0000000000077ae6 
  20.     00007ffa5157babb ntdll!RtlpAllocateHeapInternal+0x00000000000001cb 
  21.     00007ffa51479da0 msvcrt!malloc+0x0000000000000070 
  22.     00007ffa46bf3805 icm32!SmartNewPtr+0x0000000000000011 
  23.     00007ffa46bf37c8 icm32!SmartNewPtrClear+0x0000000000000014 
  24.     00007ffa46c02d05 icm32!InitNamedColorProfileData+0x0000000000000085 
  25.     00007ffa46bf6e39 icm32!Create_LH_ProfileSet+0x0000000000004e15 
  26.     00007ffa46bf1973 icm32!PrepareCombiLUTs+0x0000000000000117 
  27.     00007ffa46bf1814 icm32!CMMConcatInitPrivate+0x00000000000001f4 
  28.     00007ffa46bf12a1 icm32!CWConcatColorWorld4MS+0x0000000000000075 
  29.     00007ffa46bf11f4 icm32!CMCreateMultiProfileTransformInternal+0x00000000000000e8 
  30.     00007ffa46bf1039 icm32!CMCreateMultiProfileTransform+0x0000000000000029 
  31.     00007ffa48f16e6c mscms!CreateMultiProfileTransform+0x000000000000024c 
  32.     00007ff774651191 ldr+0x0000000000001191 
  33.     00007ff7746514b4 ldr+0x00000000000014b4 
  34.     00007ffa505a7bd4 KERNEL32!BaseThreadInitThunk+0x0000000000000014 
  35.     00007ffa515aced1 ntdll!RtlUserThreadStart+0x0000000000000021 

icm32!SwapShortOffset读取无符号的短值,对其进行bswap,并将它们存储在相同的位置,这会导致读写原语崩溃。

反编译的SwapShortOffset:

  1. unsigned __int16 *__fastcall SwapShortOffset(void *sourceBuff, unsigned int offset, unsigned int len) 
  2.   unsigned __int16 *endBuff; // r9 
  3.   unsigned __int16 *result; // rax 
  4.   
  5.   endBuff = (sourceBuff + len); 
  6.   for ( result = (sourceBuff + offset); result < endBuff; ++result ) 
  7.     *result = _byteswap_ushort(*result);        // read, bswap and write 
  8.   return result; 

崩溃的函数icm32!SwapShortOffset并不能立即指向导致该问题的根本原因。为此,我们需要查看icm32!InitNamedColorProfileData。

反编译的InitNamedColorProfileData:

  1. __int64 __fastcall InitNamedColorProfileData(__int64 a1, void *hProfile, int a3, _DWORD *a4) 
  2.   ... 
  3.   ... 
  4.   errCode = CMGetPartialProfileElement(hProfile, 'ncl2', 0, pBuffSize, 0i64);      // getting size of ncl2 element 
  5.   if ( errCode ) 
  6.     return errCode; 
  7.   minSize = pBuffSize[0]; 
  8.   if ( pBuffSize[0] < 0x55 ) 
  9.     minSize = 0x55; 
  10.   pBuffSize[0] = minSize; 
  11.   outBuff = SmartNewPtrClear(minSize, &errCode);                                    // allocating the buffer for ncl2 
  12.   ... 
  13.   ... 
  14.   errCode = CMGetPartialProfileElement(hProfile, 'ncl2', 0, pBuffSize, outBuff);    // reading ncl2 elements to buffer 
  15.   if ( !errCode ) 
  16.   { 
  17.     ... 
  18.     ... 
  19.     totalSizeToRead = count * totalDeviceCoord; 
  20.     if ( totalSizeToRead < 0xFFFFFFFFFFFFFFAEui64 && totalSizeToRead + 0x51 <= pBuffSize[0] )  // totalSizeToRead + 0x51 <= element size
  21.     { 
  22.       currPtr = outBuff + 0x54;            // wrong offset of 0x54 is used 
  23.       ... 
  24.       ... 
  25.       do 
  26.       {   
  27.         SwapShortOffset((currPtr + 0x20), 0, 6u); 
  28.         ... 
  29.         --count; 
  30.       }while(count

在这里,代码尝试读取“ncl2”标签/元素,并从文件中获取流的大小。一个缓冲区会被分配,并再次进行相同的调用,以读取元素ncl2的完整内容。该缓冲区将被解析,以查找设备位置的计数和编号,并通过确保读写操作位于缓冲区大小中来验证该值。其中存在一个漏洞,用于验证的偏移量(0x51)小于缓冲区指针的偏移量(0x54),该漏洞导致可以超过边界读写3个字节。

该漏洞的修复方法非常简单,将用于验证的偏移量修改为0x54即可,这也是Microsoft实际修复此漏洞的方式。

九、其他漏洞

在分析上面的漏洞时,我们发现过程中会使用到CMGetPartialProfileElement函数读取大小、进行分配和读取内容。而这种模式可能会引入漏洞,例如大小不受限制、整数溢出、导致偏移量增加等。我决定分析这个函数,并寻找ICM32.dll中是否存在这样的实例。

最终发现有3个实例具有未经检查的偏移量访问,分别是:CMConvIndexToNameProfile、CMConvNameToIndexProfile和CMGetNamedProfileInfoProfile。所有这些函数都可以通过导出的、有详细说明的MSCMS函数进行访问,分别是ConvertIndexToColorName、CMConvertColorNameToIndex和GetNamedProfileInfo。

反编译后的CMConvIndexToNameProfile:

  1. __int64 __fastcall CMConvIndexToNameProfile(HPROFILE hProfile, __int64 a2, __int64 a3, unsigned int a4) 
  2.   ... 
  3.   ... 
  4.   errCode = CMGetPartialProfileElement(hProfile, 'ncl2', 0, pBuffSize, 0i64);    // read size 
  5.   if ( !errCode ) 
  6.   { 
  7.     allocBuff = SmartNewPtr(pBuffSize[0], &errCode); 
  8.     if ( !errCode ) 
  9.     { 
  10.       errCode = CMGetPartialProfileElement(hProfile, 'ncl2', 0, pBuffSize, allocBuff);    // read to buffer 
  11.       if ( !errCode ) 
  12.       { 
  13.         SwapLongOffset((allocBuff + 12), 0, 4u);         // 12 > *pBuffSize ? 
  14.         SwapLongOffset((allocBuff + 16), v12, v13); 

在CMConvIndexToNameProfile和其他两个函数中,我们发现了漏洞,并没有检查ncl2元素的最小长度,且偏移量12和16可以直接访问以进行读取和写入。那么如果allocBuffer的大小小于12,就可以实现对allocBuffer的越界读写。

由于在Windows中没有二进制文件使用这些函数,因此Microsoft决定不会立即修复这三个漏洞。此外,我们也没有找到使用这些API的任何Windows软件或第三方软件。

十、总结

在这一系列文章的第一部分中,我们对颜色配置文件进行了深入分析,编写工具并成功找到了多个漏洞。请大家继续关注第二部分,在第二部分中,我们将分析很少有人研究的一类漏洞——内存未初始化漏洞。

本文翻译自:https://www.fireeye.com/blog/threat-research/2020/09/fuzzing-image-parsing-in-windows-color-profiles.html如若转载,请注明原文地址。