Instagram应用代码执行漏洞详解(三)

  • Instagram应用代码执行漏洞详解(三)已关闭评论
  • 10 views
  • A+

在上一篇文章中,我们为读者介绍wildcopy型漏洞的利用方法等内容,在本文中,我们将继续为读者介绍Mozjpeg内部的内存管理器以及相关堆布局等知识。

Mozjpeg内部的内存管理器

现在,让我们回顾一下cinfo最重要的一个结构成员,具体如下所示:

```

struct jpeg_memory_mgr mem; / Memory manager module */

```

实际上,Mozjpeg具有自己的内存管理器。这个JPEG库的内存管理器控制着内存的分配和释放,它管理着大型的“虚拟”数据数组。这个库中所有的内存和临时文件分配都是通过该内存管理器完成的。这种方法有助于防止发生存储泄漏,而且每当malloc/free操作变慢的时候,它就会加快操作速度。

该内存管理器会创建许多由空闲存内存组成的内存“池”,并且每次释放内存时,会释放掉一个内存池中的所有内存空间。

此外,对于某些数据来说,将会分配“永久性”的内存空间,直到JPEG对象被销毁时,这些内存空间才会被释放。

大多数数据是“按图像”分配的,并通过jpeg_finish_decompress或jpeg_abort函数进行释放。

例如,让我们看看Mozjpeg作为图像解码过程的一部分所进行的内存分配过程。当Mozjpeg要求分配0x108字节内存空间时,实际上就会调用malloc函数,并且分配的内存大小为0x777。正如你所看到的,请求的内存大小和实际分配的内存大小是不同的。

现在,让我们来分析一下这种行为。

Mozjpeg会分别使用两个包装器函数来分配小块内存和大块内存空间,即alloc_small和 alloc_large。
```
METHODDEF(void *)
alloc_small(j_common_ptr cinfo, int pool_id, size_t sizeofobject){
...
...
hdr_ptr = (small_pool_ptr)jpeg_get_small(cinfo, min_request + slop);
slop = first_pool_slop[1] == 16000
min_request = sizeof(small_pool_hdr) + sizeofobject + ALIGN_SIZE - 1;

sizeofobject == round_up_pow2(0x120, ALIGN_SIZE) == 0x120
ALIGN_SIZE == 16
sizeof(small_pool_hdr) = 0x20
static const size_t first_pool_slop[JPOOL_NUMPOOLS] = {
1600, / first PERMANENT pool /
16000 / first IMAGE pool /
};

```

当调用jpeg_get_mall函数时,实际上会调用malloc函数。
```
GLOBAL(void )
jpeg_get_small(j_common_ptr cinfo, size_t sizeofobject)
{
return (void
)malloc(sizeofobject);
}

```

被分配的内存池是由alloc_small和其他包装函数管理的,这些函数维护了一组成员,帮助它们监控内存池的状态。因此,每当有分配请求时,包装函数就会检查 内存池中是否有足够的剩余空间。

如果有可用空间,alloc_small函数就会从当前内存池中返回一个地址,实际上就是指向空闲空间的指针。

当内存池的空间用完时,它会使用从first_pool_slop数组中读取的预定义大小分配另一个内存池,在我们的例子中,它们分别是1600和16000。
```
static const size_t first_pool_slop[JPOOL_NUMPOOLS] = {
1600, / first PERMANENT pool /
16000 / first IMAGE pool /
};

```

现在我们了解了Mozjpeg的内存管理器是如何工作的,接下来,我们需要弄清楚哪个内存池的内存中存放着我们的目标虚函数指针。

作为解压过程的一部分,有两个主要函数用于对图像元数据进行解码,并为以后的处理准备好相应的环境:即jpeg_read_header和jpeg_start_decompress函数。同时,它们也是在执行wild copy循环之前会分配内存的仅有的两个函数。

jpeg_read_header函数用于解析文件中的不同标记。

当解析这些标记时,第二个也是最大的内存池的大小为16000 (0x3e80),它是由Mozjpeg内存管理器分配的。这些内存池的大小是来自first_pool_slop数组的const值(具体见上面的代码段),这意味着Mozjpeg的内部分配器已经使用了第一个内存池的所有空间。

我们知道,我们的目标结构体main、coef和post是从jpeg_start_decompress函数内部分配内存空间的。因此,我们可以放心地假设,其余的内存分配(直到我们到达 wildcopy 循环)将最终在第二个大型内存池中进行,包括我们想要覆盖的main、coef和post结构体。

现在让我们仔细看看Jemalloc如何处理这种粒度的内存分配操作的。

通过shadow程序实现堆的可视化

Jemalloc分配的内存空间按照规模可以分为三种粒度:小型、大型和巨型。

  • 小型/中型:这些region的长度都小于内存页的长度(通常为4KB).
  • 大型:这些region的长度介于小型/中型和巨型规模之间,也就是介于内存页和chunk的长度之间)。
  • 巨型:这些region的长度大于chunk的长度。这样的内存空间将被单独处理,而不是通过arena来管理;它们会提供一个全局分配器树。

操作系统返回的内存空间将被划分为chunk,这是Jemalloc设计中使用的最高抽象。在Android系统中,这些chunk对于不同的版本具有不同的长度。它们通常在2MB/4MB左右。每个chunk都与一个arena相关联。

一个run可以用来分配一个大型规模的内存块,或分配多个小型规模的内存块。

实际上,所有大型规模的region都具有自己的run,即分配的每个大规模内存空间都有一个专门的run。

我们知道,我们的目标内存池的长度为(0x3e80=16000 DEC),它大于内存页的长度(4K),但小于Android chunk的长度。因此,Jemalloc每次分配的内存长度,实际上是一个大型run的长度(0x5000)!

下面,让我们来仔细研究一下。
```
(gdb)info registers X0
X0 0x3fc7
(gdb)bt

0 0x0000007e6a0cbd44 in malloc () from target:/system/lib64/libc.so

1 0x0000007e488b3e3c in alloc_small () from target:/data/data/com.instagram.android/lib-superpack-zstd/libfb_mozjpeg.so

2 0x0000007e488ab1e8 in get_sof () from target:/data/data/com.instagram.android/lib-superpack-zstd/libfb_mozjpeg.so

3 0x0000007e488aa9b8 in read_markers () from target:/data/data/com.instagram.android/lib-superpack-zstd/libfb_mozjpeg.so

4 0x0000007e488a92bc in consume_markers () from target:/data/data/com.instagram.android/lib-superpack-zstd/libfb_mozjpeg.so

5 0x0000007e488a354c in jpeg_consume_input () from target:/data/data/com.instagram.android/lib-superpack-zstd/libfb_mozjpeg.so

6 0x0000007e488a349c in jpeg_read_header () from target:/data/data/com.instagram.android/lib-superpack-zstd/libfb_mozjpeg.so

```

我们可以看到,实际发送到malloc的内存分配值确实是(0x3fc7)。这与大型内存池的长度16000(0x3e80)加上Mozjpeg的large_pool_hdr的长度,以及应该被分配的对象的实际长度和ALIGN_SIZE(16/32)-1是相相吻合的。

在为了利用该漏洞而重塑堆布局的时候,如果有一种可视化堆的方法,将会带来巨大的帮助:这样的话,我们就能在堆的上下文中看到分配的内存空间。

为此,我们使用了一个简单的工具,它允许我们在利用该漏洞时检查目标进程的堆状态。实际上,我们使用的是一个名为“shadow”的工具,该工具是由argp和vats编写的,用于可视化Jemalloc堆的布局。

我们使用通过gdb启动了一个shadow调试会话,以验证我们希望覆盖的目标是大型run这一假设。
```
Cinfo:
(gdb) x/164xw 0x729f4f8b98
0x729f4f8b98: 0x9f4f89f0 0x00000072 0xbdfe3040 0x00000072
0x729f4f8ba8: 0x00000000 0x00000000 0x00000014 0x000002a8
0x729f4f8bb8: 0x00000001 0x000000cd 0xbdef79f0 0x00000072
0x729f4f8bc8: 0x00006a44 0x00009a2e 0x00000003 0x00000003
0x729f4f8bd8: 0x0000000c 0x00000001 0x00000001 0x00000000
0x729f4f8be8: 0x00000000 0x3ff00000 0x00000000 0x00000000
0x729f4f8bf8: 0x00000000 0x00000001 0x00000001 0x00000000
0x729f4f8c08: 0x00000002 0x00000001 0x00000100 0x00000000
0x729f4f8c18: 0x00000000 0x00000000 0x00006a44 0x00009a2e
0x729f4f8c28: 0x00000004 0x00000004 0x00000001 0x00000000
0x729f4f8c38: 0x00000000 0x00000000 0x00000000 0x00000001
0x729f4f8c48: 0x00000000 0x00000001 0x00000000 0x00000000
0x729f4f8c58: 0x00000000 0x00000000 0xbdef7a40 0x00000072
0x729f4f8c68: 0xbdef7ad0 0x00000072 0x00000000 0x00000000
0x729f4f8c78: 0x00000000 0x00000000 0xbdef7b60 0x00000072
0x729f4f8c88: 0xbdef7da0 0x00000072 0x00000000 0x00000000
0x729f4f8c98: 0x00000000 0x00000000 0xbdef7c80 0x00000072
0x729f4f8ca8: 0x9f111ca0 0x00000072 0x00000000 0x00000000
0x729f4f8cb8: 0x00000000 0x00000000 0x00000008 0x00000000
0x729f4f8cc8: 0xa63e9be0 0x00000072 0x00000000 0x00000000
0x729f4f8cd8: 0x00000000 0x00000000 0x00000000 0x00000000
0x729f4f8ce8: 0x00000000 0x01010101 0x01010101 0x01010101
0x729f4f8cf8: 0x01010101 0x05050505 0x05050505 0x05050505
0x729f4f8d08: 0x05050505 0x00000000 0x00000000 0x00000101
0x729f4f8d18: 0x00010001 0x00000000 0x00000000 0x00000000
0x729f4f8d28: 0x00000000 0x00000000 0x00000002 0x00000002
0x729f4f8d38: 0x00000008 0x00000008 0x000009a3 0x00000000
0x729f4f8d48: 0xa63e9e00 0x00000072 0x00000003 0x00000000
0x729f4f8d58: 0xa63e9be0 0x00000072 0xa63e9c40 0x00000072
0x729f4f8d68: 0xa63e9ca0 0x00000072 0x00000000 0x00000000
0x729f4f8d78: 0x000006a5 0x000009a3 0x00000006 0x00000000
0x729f4f8d88: 0x00000000 0x00000000 0x00000000 0x00000001
0x729f4f8d98: 0x00000002 0x00000000 0x00000000 0x00000000
0x729f4f8da8: 0x00000000 0x00000000 0x0000003f 0x00000000
0x729f4f8db8: 0x00000000 0x00000008 0xa285d500 0x00000072
0x729f4f8dc8: 0x0000003f 0x00000000 0xbdef7960 0x00000072
0x729f4f8dd8: 0xa63eaa70 0x00000072 <========= main
0xa63ea900 0x00000072 <========= post
0x729f4f8de8: 0xa63ea3e0 0x00000072 <========= coef
0xbdef7930 0x00000072
0x729f4f8df8: 0xbdef7820 0x00000072 0xa63ea790 0x00000072
0x729f4f8e08: 0xa63ea410 0x00000072 0xa63ea2c0 0x00000072
0x729f4f8e18: 0xa63ea280 0x00000072 0x00000000 0x00000000

(gdb) jeinfo 0x72a63eaa70 <========= main
parent address size


arena 0x72c808fc00 -

chunk 0x72a6200000 0x200000
run 0x72a63e9000 0x5000 <========= our large targeted run!

```

堆布局的重塑

我们的目标是利用一个整数溢出,导致堆缓冲区溢出。

利用这类漏洞的重点在于对堆对象进行精确定位。我们要强制某些对象被分配到堆中的特定位置,这样我们就可以形成有用的邻接关系来进行内存破坏。

为了实现这种相邻关系,我们需要对堆的布局进行重塑,使我们的可利用对象刚好分配在我们的目标对象之前。

不幸的是,我们无法控制释放操作。根据Mozjpeg的文档的说法,“大部分数据都是“按图像”分配的,并由jpeg_finish_decompress函数,或jpeg_abort函数进行释放”。这意味着,所有的释放操作都发生在使用jpeg_finish_decompress,或者jpeg_abort函数的解压过程结束时,而jpeg_abort函数只有在我们用wildcopy循环完成覆盖内存操作后才会被调用。

然而,在我们的例子中,我们不需要任何释放操作,因为我们可以控制一个函数:该函数执行一个原始malloc调用,而分配的内存空间的长度处于我们的控制之下。这使我们能够选择将溢出缓冲区放置在堆上的位置。

我们希望将包含我们溢出缓冲区的对象定位在包含main/post/coef数据结构的大型(0x5000)对象之前,并由该对象执行对函数指针的调用。

1.png
图7 可视化堆上的Jemalloc对象

因此,对我们来说,最简单的利用方法是对堆的布局进行重塑,使溢出的缓冲区恰好分配到我们的目标大型对象(0x5000)之前,然后(利用该漏洞)用我们提供的地址覆盖main/post/coef虚函数的地址。这样我们就可以完全控制虚拟表,从而将任何方法重定向到任何代码地址。

我们知道,目标对象的长度总是大型规模的长度,即0x5000,并且Jemalloc是按照自顶到底的方向来分配大型规模长度的内存空间的,所以,我们唯一需要做的,就是将我们的溢出对象放在大型目标对象所在的同一个chunk的底部。

在我们测试的Android版本中,Jemalloc的chunk大小为2MB。

需要注意的是,对象之间的距离(以字节为单位)并不重要,因为我们有一个wildcopy循环,可以逐行复制大量的数据(我们能够控制行的大小)。由于被复制的数据的长度最终会大于2MB,所以我们可以肯定,我们最终会破坏位于我们溢出对象之后的chunk上的每个对象。

由于我们对内存释放操作没有任何控制权,因此,我们无法创建令我们的对象落入其中的hole。在这里,hole是指run中的一个或多个空闲内存块。相反,我们尝试寻找作为图像解压缩流的一部分无论如何都会发生的漏洞,寻找调试期间每次重复的大小。

接下来,让我们使用shadow工具来考察chunk在内存中的布局情况。
```
(gdb) jechunk 0x72a6200000
This chunk belongs to the arena at 0x72c808fc00.
addr info size usage


0x72a6200000 headers 0xd000 -

0x72a620d000 large run 0x1b000 -

0x72a6227000 large run 0x1b000 -

0x72a6228000 small run (0x180) 0x3000 10/32

0x72a622b000 small run (0x200) 0x1000 8/8

...
...
0x72a638f000 small run (0x80) 0x1000 6/32

0x72a6390000 small run (0x60) 0x3000 12/128
0x72a6393000 small run (0xc00) 0x3000 4/4

0x72a6396000 small run (0xc00) 0x3000 4/4

0x72a6399000 small run (0x200) 0x1000 2/8

0x72a639a000 small run (0xe0) 0x7000 6/128 <===== The run we want to hit!!!
0x72a63a1000 small run (0x1000) 0x1000 1/1

0x72a63a2000 small run (0x1000) 0x1000 1/1

0x72a63a3000 small run (0x1000) 0x1000 1/1

0x72a63a4000 small run (0x1000) 0x1000 1/1

0x72a63a5000 large run 0x5000 - <===== Large targeted object!!!

```

我们正在寻找的是含有hole的run,这些run必须位于我们要覆盖的大型目标缓冲区之前。一个run可以用来分配一个大型规模的内存空间,或者多个小型/中型规模的内存空间。

用于分配小型规模内存空间的run会被划分为众多region。而一个region就是一个小型规模内存空间的同义词。每个小型run中的region必须具有相同的长度。换句话说,一个小型run必定与一个特定的region粒度相关联。

用于分配中等规模内存空间的run也会被划分为多个region,但正如其名称所暗示的那样,它们的长度要比分配小型内存空间的长度要大一些。因此,用于分配中等规模内存空间的run将被划分为更大粒度的region,而粒度越大,占用的内存空间也更多。

例如,一个粒度为0xe0的小型run将被划分为128个region:

```
0x72a639a000 small run (0xe0) 0x7000 6/128

```

一个粒度为0x200的中等规模run可以划分为8个region:

```
0x72a6399000 small run (0x200) 0x1000 2/8

```

由于分配小型内存空间是最常见的情况,所以,我们需要操纵/控制/溢出的内存空间,就是属于这种。由于要分配的小型内存空间将被划分为更多的region,因此它们更易于控制,因为其他线程分配所有其余region的可能性较小。

因此,为了使可溢出对象在大型目标对象之前被分配,我们可以使用前面的Python脚本。该脚本可帮助我们生成相应的维度,这些维度将导致malloc函数以所需的小型粒度来分配可溢出对象。

我们构造了一个新的JPEG图像,其长度正好可以导致为对象分配的内存空间的粒度为小型(0xe0),并在libjepgutils_moz.so+0x918处设置一个断点。
```
(gdb) x/20i $pc
=> 0x7e47ead7dc: bl 0x7e47eae660 __wrap_malloc@plt
0x7e47ead7e0: mov x23, x0

```

我们位于受控的malloc之前的一条指令处,X0存放的是我们想要分配的内存空间的长度:
```
(gdb) info registers x0
x0 0xe0 224

```

我们继续执行一条指令,然后再次检查X0寄存器,该寄存器现在保存的是malloc返回的结果:
```
(gdb) x/20i $pc
=> 0x7e4cf987e0: mov x23, x0

(gdb) info registers x0
x0 0x72a639ac40 492415069248

```

从malloc函数返回的地址就是可溢出对象(0x72A639AC40)的地址。接下来,让我们使用shadow框架中的jeinfo方法检查它在堆中的位置。
```
(gdb) jeinfo 0x72a639ac40
parent address size


arena 0x72c808fc00 -

chunk 0x72a6200000 0x200000
run 0x72a639a000 0x7000

region 0x72a639ac40 0xe0

```

我们与大型目标对象位于同一个chunk(0x72A6200000)中!现在,让我们再次查看该chunk的布局情况,以确保溢出缓冲区位于小型粒度(0xE0)的目标内存区中。

```
(gdb) jechunk 0x72a6200000
This chunk belongs to the arena at 0x72c808fc00.


...
0x72a639a000 small run (0xe0) 0x7000 7/128 <-----hit!!!
0x72a63a1000 small run (0x1000) 0x1000 1/1

0x72a63a2000 small run (0x1000) 0x1000 1/1

0x72a63a3000 small run (0x1000) 0x1000 1/1

0x72a63a4000 small run (0x1000) 0x1000 1/1

0x72a63a5000 large run 0x5000 - <------Large targeted object!!!

```

好的!现在让我们继续执行,看看当我们覆盖大型目标对象时会发生什么。
```
(gdb) c
Continuing.
[New Thread 29767.30462]

Thread 93 "IgExecutor #19" received signal SIGBUS, Bus error.

0xff9d9588ff989083 in ?? ()

```

嘭!这正是我们的目标——当我们试图通过函数指针从可溢出对象中加载破坏数据的函数的地址时发生了崩溃。我们触发了一个总线错误(也称为SIGBUS,通常是信号10),当一个进程试图访问CPU无法物理寻址的内存时,就会发生这种错误。换句话说,程序试图访问的内存不是一个有效的内存地址,因为它包含了我们图像中的数据,取代了真正的函数指针,并导致了这次崩溃!

小结

在本文中,为读者介绍Mozjpeg内部的内存管理器以及相关堆布局等知识。在接下来的文章中,我们将综合上面的知识点,进一步探讨该漏洞的可利用性。

原文地址:https://research.checkpoint.com/2020/instagram_rce-code-execution-vulnerability-in-instagram-app-for-android-and-ios/

相关推荐: 使用iOS 0day窃取您的SMS消息

译文声明 本文是翻译文章,文章原作者Wojciech Reguła 文章来源:https://wojciechregula.blog 原文地址:https://wojciechregula.blog/post/stealing-your-sms-message…