Android中的Unicode国际组件(ICU)库

什么是ICU?

ICU是一个基于Unicode的跨平台全球化库。它包括对区域设置敏感的字符串比较,日期/时间/数字/货币/消息格式,文本边界检测,字符集转换等支持。
软件国际化开发过程使用库(例如Unicode国际组件(ICU)库)来使一个程序能够与世界上任何地方的任何语言的文本一起使用。例如,ICU服务可以创建一个可以在所有国家/地区无缝且透明地运行的版本,而不必为十个不同的国家/地区提供单独的软件版本。
ICU组件是软件开发的组成部分,因为它们隐藏了特定于区域设置的软件要求的文化差异和技术复杂性。这些复杂性为应用程序提供了关键功能,但是应用程序开发人员无需花费巨大的精力或花费高昂的成本来构建它们。
Unicode国际组件既可以作为C / C ++库也可以作为Java类库使用。ICU4C(ICU)用C和C ++编写,而ICU4J用Java™编写。

具体可参见官方用户指南:http://userguide.icu-project.org/

Android源码external下引入的icu4c库具有超出界限的写入,这是由于与common / utext.cpp中的utf8TextAccess函数和utext_setNativeIndex *函数以及utext_moveIndex32 *函数相关的基于堆的缓冲区溢出引起的。CVE号:CVE-2017-7867-7868

最近验证这个问题花了不少时间,在这里把做过的分析总结一下。
我们使用patch中官方给出的单元测试来验证。具体参见:https://gitlab.alpinelinux.org/alpine/aports/-/blob/aed0c2d1a6d20e48cdc0ecaff89883d567d7331d/main/icu/CVE-2017-7867-7868.patch
测试程序被作为测试模块的case Ticket 12888()加入到代码中,这是一个非法的六字节utf-8格式的测试。由于系统代码中UTF8Buf的mapToUChars当前长度是根据ucs-2规则存储的,因此utf-8的长度为1-3个字节。 该补丁遵循ucs-4规则将utf-8的长度扩展为1-6个字节来修复漏洞。

首先来看Android4.2.2的代码里:

测试文件在external/icu4c/test/intltest/下,
我们调查utxttest.cpp的UTextTest::runIndexedTest()的调用顺序发现是通过测试类的基类intltest.cpp的callTest()函数调用的。
然后intltest.cpp的main()函数调用ctest_xml_init()为测试可执行文件intltest文件中写数据,再调用runTest()和runTestLoop()执行测试。
于是,我们编译external/icu4c模块后执行intltest文件,执行成功了,证明单元测试在x86平台环境下可行。
接着合入patch编译后执行报错,程序崩溃了,如图:


合入patch后Ticket12888执行失败

从堆栈信息可以看出Ticket12888的执行需要依赖libicuuc.so.48这个库,而external/icu4c模块的libicuuc.so.48库编译在external/icu4c/tools/ctestsw/路径下。测试程序执行时调用的是usr/local/lib下的libicuuc.so.48库,所以会执行失败。
我们尝试替换usr/local/lib下的icu4c库所用到的包为我们代码中编译出来的库,继续执行intltest文件。结果执行成功。如图:


替换库后单元测试执行成功

合入patch后程序顺利执行,有非法字符提示的log,但程序最终是执行成功的,说明合入patch有效的修补了漏洞。
于是,我们考虑分离Ticket12888作为独立的测试程序在单板上执行。由于arm平台的编译与x86不同,使用-L指定库的方式来链接我们编译出来的icu库,结果失败。最后在编译文件的LOCAL_SHARED_LIBRARIES属性指定链接库为system/lib/libicuuc.so后执行成功。

******************************************************************分割线***********************************************************

再来看Android6.0的代码里:

测试文件在external/icu/icu4c/source/test/intltest/下,
首先,合入patch之前执行测试文件来验证,如图:

可以看见测试没有执行成功,程序崩溃提示free异常,并打出了一些异常堆栈信息。
经过调查发现,测试程序Ticket12888()在执行完测试程序代码后报出了free错误。这个漏洞是测试程序执行完后,调用ut的析构函数执行utext_close释放内存时,由于在测试程序写入过程中超出了UTF8Buf结构体开辟的空间存贮量而导致堆缓冲区溢出。这使uprv_free释放内存失败。

然后,合入patch再次执行测试文件,执行结果如下图:

测试程序没有错误信息执行成功。
但这里和4.2.2代码的执行结果有差异,失败的报错变了,为什么不一样?为什么成功的变量c也没有了非法的参数65535??心脏有点受不住



这个结果让人意识到需要找到漏洞所在的代码行,否则会随着测试结果的改变而一直被困扰。于是又开始了一段探索之路。。

从测试程序变量c赋值的地方开始,查看调用的函数。从4.2.2的代码入手:
1.根据测试程序中调用的函数追踪,加入log打印后分析,根据参数值的变化和调用顺序,看不出太多有效信息来解答疑问
2.从G2和G2.5漏洞存在的函数utf8TextAccess对比差异入手,发现fillReverse:中对于c赋值的地方确实有略微差异
3.之后从结构体本身入手,追踪其中变量在合入patch前后的变化,对比发现值存在很大差异,start的值没有从0开始,有点线索和眉目;后在差异处加入log打印,有打出来log,但依然找不到在哪里赋值出现了变化
4.然后从错误信息入手,调查是从哪里开始出现错误的,发现测试程序是全部执行完毕的,于是发现LocalUTextPointer ut(utext_openUTF8(NULL, badString, -1, &status));只有这句可能会出问题,LocalUTextPointer是个指针函数,执行完毕会调用默认的析构函数,在析构函数中执行utext_close释放内存时,由于释放的内存超过了malloc分配的内存而导致uprv_free释放内存失败。于是在utext_openUTF8的uprv_malloc时打印出需要的内存值和uprv_free时的值做对比,差异很大,得出结论。
5.之后调查65535和65533的问题,加log循环打印出ut->chunkContents[index]的值,发现之后有调用fillReverse:处理c的值。跟踪处理流程在执行的函数前后加打印观察变化,找到了赋值62235的地方。(if 0x10000)
6.最后追踪为什么G2.5执行成功而没有赋值成62235,又从G2和G2.5漏洞存在的函数utf8TextAccess对比差异入手,发现G2.0调用utf8_prevCharSafeBody(s8, 0, &srcIx, c, -1)时传入的参数是-1,G2.5时传入的-3。在之后的代码中G2.5调用了errorValue()如果传入-3默认全部return 0xfffd也就是65533,而G2返回的是-1,也就是65535.(于是发现吃了进制间转换不敏感的亏)

你可能感兴趣的:(Android中的Unicode国际组件(ICU)库)