三星Qmage编解码器及远程攻击面简介

2020-07-17 03:11:37

这篇文章是由多个部分组成的系列文章的第一部分,记录了我从发现一个鲜为人知的易受攻击的三星图像编解码器,到完成对最新三星旗舰设备有效的远程零点击彩信攻击的历程。新帖子将在完成时发布,并将在完成时链接到此处。

在2020年1月,我报告了一个名为Qmage&34;的三星定制编解码器中出现的大量崩溃事件,该编解码器自2014年末(Android版本4.4.4+)以来一直存在于所有三星手机中。这个编解码器是用C/C++代码编写的,并且深入到Skia图形库中,而Skia图形库又是用于Android操作系统中几乎所有图形操作的底层引擎。换句话说,除了众所周知的JPEG和PNG等格式外,现代三星手机本身也支持专有的Qmage格式,通常由.qmg文件扩展名表示。它对所有显示图像的应用程序都会自动启用,使其成为远程攻击的主要目标,因为发送图片是一些最受欢迎的移动应用程序的核心功能。

2020年5月,三星为有资格接收安全更新的设备发布了解决崩溃(包括一些缓冲区溢出和其他内存损坏问题)的补丁。这些问题被集体分配给CVE-2020-8899和三星特有的SVE-2020-16747。在安全公告发布的当天,我取消了相关的#2002跟踪器条目的限制,并在GitHub(SkCodecFuzzer)上开源了我的毛茸茸。我建议阅读原始报告,因为它详细解释了编解码器当时的当前状态、我对其进行模糊化的方法,以及由此发现的bug。它可能会提供一些有价值的上下文,以便更好地理解这篇文章和本系列中即将发布的其他博客文章,尽管我会尽最大努力使它们自成一体,易于理解。

在报告了这些漏洞之后,我在接下来的几个月里试图为其中一款旗舰手机--运行Android 10的三星Galaxy Note 10+建立一个零点击彩信漏洞。作为参考,谷歌零点计划(Google Project Zero)的塞缪尔·格罗伯(Samuel Gro«)和娜塔莉·西尔瓦诺维奇(Natalie Silvanovich)在2019年通过iMessage对聊天应用进行了类似的攻击(参见演示视频和博客帖子#1、#2、#3)。另一方面,据我所知,自2015年披露Stagefright漏洞以来,还没有公开记录过针对Android的此类攻击企图。对我来说,这似乎是一个很好的机会,可以深入研究目前Android上的漏洞缓解情况,看看他们对一个相对强大的漏洞的反应如何-一个基于堆的缓冲区溢出,缓冲区大小、数据长度和数据本身都受到了控制。最后,我设法开发了一个漏洞,它远程绕过了ASLR,在受害者的手机上(拥有SMS/MMS应用程序的特权)获得了一个反向外壳,平均只需大约100分钟,不需要用户交互。攻击演示的官方录音可以在这里获得,下面我将介绍同一视频的导演剪辑,并为您的观看乐趣添加了配乐。:)。

通过发布这篇文章以及Qmage系列中的更多博客文章,我希望更多地了解我是如何找到编解码器的,在侦察和准备Fuzing的过程中了解到了什么,以及我是如何在编写可靠的MMS漏洞的过程中设法绕过各种Android缓解措施和障碍的。请和我一起坐这趟车吧!

就像脆弱性研究中经常发生的情况一样(根据我的经验),在发现这个攻击面时有一点运气。2019年末,Project Zero举办了一场黑客松,作为团队凝聚活动的一部分,重点是三星手机。我们选择三星手机是因为它们是2019年第二季度欧洲最受欢迎的移动设备,当时我们正在计划我们的活动。该事件的其他结果也可以在PZ错误跟踪器中找到-它们主要围绕三星内核模式组件的安全展开,并在2020年2月修复。在黑客马拉松期间,我们使用了运行Android 9的三星Galaxy A50作为我们的测试设备,但本文的其余部分是基于对更新的Note 10+和Android 10的分析,这两款设备在本研究期间都是最新的三星旗舰设备。

当我寻找潜在的bug搜索目标时(自然是用本地语言编写的,这样就会出现内存损坏问题),我的第一个想法是从bug跟踪器和过去的现有报告中寻找灵感。在那里,娜塔莉在2015年报道的以下问题立即引起了我的注意:

图像处理中的内存安全问题,多么精彩!请在某些书目中注意";libQjpeg&34;库名称中的";q&34;-在不知不觉中,这是我第一次遇到第三方Quramsoft软件供应商。我深入研究了错误描述,但我不能。

如果您想知道目录中还有多少名称中含有quram的库,那么Note 10+上还有三个:

通常,特定的三星Android版本中使用的各种Quram库都列在/system/etc/public.library-quram.txt中。我认为值得强调的是,Quramsoft拥有一系列与音频、视频、图像和动画相关的软件解决方案,包括编码和解码。在Android问世的整个过程中(甚至在此之前),三星一直与这家第三方供应商密切合作,并在他们的定制操作系统版本中包含了许多他们的库,主要是为了支持和推动内置应用程序,如相机或Gallery。多年来,这些库一直在发展,其中一些被重新命名和移除,而另一些则被重构和合并,以至于公众客观上很难跟踪哪些三星机型安装了哪些库的子集。然而,他们中的许多人仍然存在,并在最新的手机上使用。我希望这有助于澄清我为什么在Qmage编解码器的上下文之外引用Quram库的任何困惑。

回到故事-在简短的分析之后,我将我的兴趣范围缩小到libimagecodec.quram.so库,它是最大的、导入最多的库,似乎实现了对各种图像格式的支持。我可以很容易地通过Gallery触发它,但我仍然很难通过媒体扫描到达它,这是娜塔莉在她的许多虫子中用作攻击媒介的东西。我开始研究媒体扫描器是如何工作的,从aosp中的platform/frameworks/base/media/java/android/media/MediaScanner.java开始,特别是scanSingleFile→doScanFile→process ImageFile路径。在这里,我们可以看到,它真正归结为标准BitmapFactory接口的使用:

后来,我验证了我测试的三星设备上的MediaScanner服务使用了非常相似的代码;唯一的区别是额外引用了com.samsung.android.media.SemExtendedFormat接口和相关的libSEF.quram.so库,这似乎与我触发Quram的自定义JPEG解码器的目标无关。

当时我对Android和它的图形子系统不是很熟悉,我想更深入地挖掘抽象堆栈,看看实际的图像解码代码。为了实现这一点,我遵循了另外几个嵌套方法的执行路径,首先是在Java语言中,然后进入C++领域:BitmapFactory.decdeFile→decdeStream→decdeStreamInternal→nativeDecodeStream→doDecode。在这里,我们终于可以看到从字节流解码图像的实际逻辑,首先通过创建Skia SkCodec对象:

因此,为了了解该接口支持哪些图像格式,我们必须查看SkCodec::MakeFromStream。该方法的上游版本可以在GitHub上找到;在那里,我们可以看到,根据编译期间定义的宏,可以加载以下类型的图像(主要基于gDecoderProcs表):

这已经是一个相当大的格式列表。我们可以在/system/lib64/libhwui.so中将开源实现与三星手机上的编译实现进行比较,这就是现在Android上Skia代码所在的位置(在旧系统上,它位于/system/lib64/libska.so)。当我最初在IDA Pro中打开SkCodec::MakeFromStream方法时,我看到一个展开的循环在标准Skia编解码器上迭代,但也有一些额外的文件签名检查,即:

在简要分析之后,我得出结论,从安全研究的角度来看,PIO和ASTC并不是特别有趣,我将目光转向了Qmage。考虑到libhwui.so有数百个包含";quram";、";qmage";或其他相关字符串的函数,这些例程执行低级文件格式解析,并且其中许多都非常长,因此它看起来是显而易见的选择。编解码器看起来如此复杂,如此深入地集成到Android中,这让我真的很感兴趣。造成这一切的另一个因素是,我以前从未听说过它,即使使用谷歌搜索也没有太大帮助。在攻击性安全中,这通常是一个非常有吸引力的研究目标的非常强烈的指标,所以我别无选择-我不得不戴上我的侦探帽子,进行进一步的调查。

在这个阶段,我有很多问题在脑海中徘徊,几乎没有答案:

它背后的历史是什么?它发货多长时间了?三星所有的手机都有这个功能吗?

我花了好几个星期才找到答案,但为了简短起见,我将对这些事件进行加速描述,跳过一些混乱的时期,并试图弄清楚当时可用的部分信息的意义。:)。

到目前为止,我们知道Qmage文件有两个可能的魔术值:QM和QG。如果我们更深入地研究QuramQmageDecVersionCheck→QmageDecCommon_VersionCheck,这是头检查的第二部分,我们将看到以下逻辑(使用类似C的伪代码):

该函数再次验证QG签名,然后将接下来的两个字节作为版本标识符。如果我们假设数据[2]和数据[3]分别是主版本号和次版本号,那么根据上面的代码,支持版本≤2.0。事实上,这是实现检查的一种非常宽松的方式,因为它允许通过一些并不真正存在的版本。在撰写本文时,我已经知道QG格式有三个实际有效的版本:

编解码器会忽略主要/次要版本(如1.231)的其他组合,或者将其解析为以上三个版本之一。

要了解有关QM映像版本控制的更多信息,我们可以类似地遵循反汇编程序中的QuramQmageDecVersionCheck_Rev8253_140615→QmageDecCommon_VersionCheck_Rev8253_140615函数,这将引导我们进入以下逻辑:

这绝对比预期的代码多。我们主要对最后一条if语句感兴趣,在该语句中,我们可以看到在QM魔术之后应该有一个0x01字节。再次假设这是版本号,我们可以注意到现代三星版本的Skia支持QM格式的版本1。但是,还有一些其他签名正在检查:IM、IT、IFEG、QW和PFR。我不知道它们具体代表什么格式,而且由于上面的例程只能通过在SkCodec::MakeFromStream中检测到的QM头到达,签名看起来并不是有意的。更有可能的是,它们是显示由其他地方的Quramsoft代码解析的文件格式的遗留工件,或者已被弃用,根本不再使用。我们将来可能会再次看到这些常量,因此值得记住它们。

总之,Skia支持的Qmage有四个不同的版本,按时间顺序是:QMv1、QG1.0、QG1.1、QG2.0。查看libhwui.so中的调试符号列表时,这一点尤为明显。对于QMv1编解码器中一直存在到QG2.0的每个符号,现在都有给定变量/函数/等的四个副本,例如:

不带任何后缀的名称表示最新格式(QG2.0)的部分代码。对于每种较早的格式,似乎都有一个代码分叉,所有函数、结构、静态对象等都重命名为包括修订号和第二个看起来像日期的数字部分。如果我的理解是正确的,这将意味着QMv1、QG1.0和QG1.1版本的截止日期在2014年6月、2014年10月和2015年2月左右-这是一个相对较短的时间段。QG2.0版本最早出现在2020年1月的Android 10中,但作为最近的版本,它没有方便的_RevXXXX_YYMMDD后缀来告诉我们确切的修订号。

然而,在预编译的Skia二进制文件中还可以找到关于编解码器本身的版本控制的其他一些信息。例如,在libhwui.so的当前版本中,有一个名为QmageDecCommon_QmageVersion_1_11_00的空函数。此外,库中还有一个未使用的QuramQmageGetDecoderVersion函数,该函数打印出其他类型的由四部分组成的版本号以及确切的构建日期和时间,例如:

如果版本号由四个X.Y.Z.R整数表示,则X.Y对表示编解码器支持的QG格式的最高版本(在本例中为QG2.0),而R是代码的修订号。通过研究在三星多年来发布的存档固件中发现的无数libska.so和libhwi.so版本,人们可以非常准确地记录三星设备附带的所有不同的Qmage编译。我有限的分析得出了以下毫无疑问不完整的表格:

根据上述资料,我们可以确认一些现有的假设,并得出新的结论:

2014年至2016年期间,它的开发活动最活跃,随后几年相对不活跃,再次推出了该格式的新版本2.0,该版本于2019年9月首次编译用于生产使用。

版本(4)的Z组件和修订号(21541)自2016年以来从未更改过,这限制了我们对代码库最近变化的洞察力。

对于像我这样对小段有趣的元数据感兴趣的人来说,在库中可以找到更多的工件。例如,在Samsung Android 10上,libhwui.so中的QuramQmageGetDecoderVersion函数有三个副本(对于每个版本的QG格式),并且系统中存在共享对象的32位和64位版本,因此对于一次Qmage编解码器编译,我们将在该过程中获得六个不同的时间戳。在2019年9月26日的构建示例中:

我不认为有任何信息可以完全确定地从中得出,但我仍然觉得它足够吸引人,可以把它包括在这里。至少,第一个和最后一个时间戳之间的2m36s间隔为我们提供了有关编解码器复杂程度的线索。

当我们在IDA Pro中打开libhwi.so并开始检查编译形式的Qmage代码时,上面的构建时间可能会开始有意义。在我测试过的几个库版本中,与Qmage相关的代码被放在一个连续的二进制blob中,这使得测量它的大小变得很容易。例如,在2019年9月26日版本(2.0.4.21541)中,Qmage相关块中的第一个函数是QmageDecoderLicenseCheck,最后一个函数是SetResidualCoeffs_C。它们之间的总代码区大小几乎为908kB(!),约占共享对象中全部可执行代码段的15%。这在很大程度上要归功于Android 10中添加的QG2.0编解码器,它引入了更多的代码复制(大多数Qmage相关函数的新分支),并导入了一个全新的libwebp副本。但即使是在Android 9和2017年11月8日的版本上,编解码器也有大约425kB的长度。

下面是在libhwui.so中找到的20个最长函数的列表。请注意其中18/20是如何与QMage相关的:

现在让我们移到编解码器的控制流和入口点上。当通过BitmapFactory等Android接口加载Qmage图像时,执行以doDecode函数结束,该函数随后调用SkCodec::MakeFromStream,如上所述。然后,如果前几个字节与";QG&34;签名匹配,则执行将到达SkQmgCodec::MakeFromStream和更多用于标题解析的嵌套函数:

较旧的QMv1文件的流程非常相似。此基本解析足以提取有关位图的基本信息,如其尺寸,因此如果在BitmapFactory.Options中设置了inJustDecodeBound标志,则文件的处理将在此结束。然而,尽管与完整的位图解码相比,标题解析逻辑简短而简单,我仍然设法发现了与在内存中构建颜色表相关的内存损坏问题。因此,即使是只查询不可信图像边界的进程,比如MediaScanner服务,也容易受到Qmage的攻击。但我们不要操之过急。如果调用方请求完整的位图数据(例如,让应用程序显示它),则执行将继续进行到SkQmgCodec::onGetPixels并更深入:

到目前为止,连续的函数大多是下一个嵌套例程的简单包装器,不涉及太多数据处理逻辑。这随PVcodecoder而改变[.]。最终选择相关的低级编解码器,并调用执行繁重任务的相应长而复杂的函数之一,如PVcodecdecoder_1channel_32bits_new或QuramQumageDecoder32bit24bit。可用压缩类型的子集在不同的Qmage版本之间有所不同;我已经执行了一个粗略的分析,并将它们记录在下表中。其中一些实现了众所周知的概念,如游程编码(RLE)或zlib膨胀,而另一些(最复杂的)似乎执行自定义的专有解压缩算法。

值得注意的是,即使在最新版本的编解码器中不再存在其中的一些压缩类型,它们仍然存在,并且可以通过三星设备支持的旧版本访问。一个小的例外是QMv1格式,它有时在某些上下文中无法加载到Skia中,可能是因为它过时了,并且没有在现代设备上进行适当的测试。

同样有趣的是,在这个抽象级别,QG2.0格式本身并没有引入任何新的编解码器。这并不意味着与QG1.1相比,它只是一个很小的修订-相反,它确实引入了大量新函数,只是在调用层次结构的不同级别:

QG2.0格式似乎没有添加新的压缩类型,而是在现有压缩类型的基础上进行了构建和改进。它还导入了zlib 1.2.8库,其每个函数都带有qme_前缀,并导入了libwebp的完整第二个副本(其中一个已经被Skia使用),其所有符号都附加了_qmg后缀。人们可以假设,libwebp的添加表明了某种WebP-in-qmage;特性,但这是基于这样一个事实,即该库的大部分从未被引用过。

.