CVE-2015-1538 libstagefright漏洞分析

漏洞分析

POC直接使用https://github.com/jduck/cve-2015-1538-1生成的mp4文件,作者说明了这个exp并不通用,我的测试机为nexus-5-android-4.4,只能造成崩溃。
崩溃信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
03-13 13:19:33.130 14267 14267 I DEBUG   : *** *** *** *** *** *** *** *** *** *** *** *** *** *** *** ***
03-13 13:19:33.130 14267 14267 I DEBUG : Build fingerprint: 'google/hammerhead/hammerhead:4.4.4/KTU84P/1227136:user/release-keys'
03-13 13:19:33.130 14267 14267 I DEBUG : Revision: '11'
03-13 13:19:33.130 14267 14267 I DEBUG : pid: 17874, tid: 17928, name: Binder_1 >>> /system/bin/mediaserver <<<
03-13 13:19:33.130 14267 14267 I DEBUG : signal 11 (SIGSEGV), code 1 (SEGV_MAPERR), fault addr 41d00010
03-13 13:19:33.140 15745 15836 W NativeCrashListener: Couldn't find ProcessRecord for pid 17874
03-13 13:19:33.180 14267 14267 I DEBUG : r0 41d00010 r1 00d00000 r2 001ed3c9 r3 00000000
03-13 13:19:33.180 14267 14267 I DEBUG : AM write failure (32 / Broken pipe)
03-13 13:19:33.180 14267 14267 I DEBUG : r4 b5b40030 r5 000000da r6 2a0480a0 r7 00000a38
03-13 13:19:33.180 14267 14267 I DEBUG : r8 0000000c r9 b5b40034 sl 00000008 fp 2a0480a4
03-13 13:19:33.180 14267 14267 I DEBUG : ip 41d00000 sp b5b40010 lr b697edaf pc b697ed3a cpsr 000f0030
03-13 13:19:33.180 14267 14267 I DEBUG : d0 0000001000000079 d1 0000000000000061
03-13 13:19:33.180 14267 14267 I DEBUG : d2 7472616b6d6f6f64 d3 6d696e666d6469a9
03-13 13:19:33.180 14267 14267 I DEBUG : d4 4242424242424242 d5 4242424242424242
03-13 13:19:33.180 14267 14267 I DEBUG : d6 4242424242424242 d7 4242424242424242
03-13 13:19:33.180 14267 14267 I DEBUG : d8 0000000000000000 d9 0000000000000000
03-13 13:19:33.180 14267 14267 I DEBUG : d10 0000000000000000 d11 0000000000000000
03-13 13:19:33.180 14267 14267 I DEBUG : d12 0000000000000000 d13 0000000000000000
03-13 13:19:33.180 14267 14267 I DEBUG : d14 0000000000000000 d15 0000000000000000
03-13 13:19:33.180 14267 14267 I DEBUG : d16 3f96de83a904ab51 d17 3f50624dd2f1a9fc
03-13 13:19:33.180 14267 14267 I DEBUG : d18 41cc356335800000 d19 3fc35fe27b800000
03-13 13:19:33.180 14267 14267 I DEBUG : d20 3fc5533fc2a45f6a d21 bf66be3f0c22dc9a
03-13 13:19:33.180 14267 14267 I DEBUG : d22 3fc2e2cedd55ea5c d23 3fe0000000000000
03-13 13:19:33.180 14267 14267 I DEBUG : d24 3f88b0d283c30939 d25 bf88b0d28a518506
03-13 13:19:33.180 14267 14267 I DEBUG : d26 0000000000000000 d27 4000000000000000
03-13 13:19:33.180 14267 14267 I DEBUG : d28 3ffda3a6245542b4 d29 bfc4eaefa4251850
03-13 13:19:33.180 14267 14267 I DEBUG : d30 3ff0000000000000 d31 3fe29d5df484a30a
03-13 13:19:33.180 14267 14267 I DEBUG : scr 60000010
03-13 13:19:33.180 14267 14267 I DEBUG :
03-13 13:19:33.180 14267 14267 I DEBUG : backtrace:
03-13 13:19:33.180 14267 14267 I DEBUG : #00 pc 0007dd3a /system/lib/libstagefright.so (android::SampleTable::setSampleToChunkParams(long long, unsigned int)+137)
03-13 13:19:33.180 14267 14267 I DEBUG : #01 pc 00062f8d /system/lib/libstagefright.so (android::MPEG4Extractor::parseChunk(long long*, int)+2816)
03-13 13:19:33.180 14267 14267 I DEBUG : #02 pc 0006271f /system/lib/libstagefright.so (android::MPEG4Extractor::parseChunk(long long*, int)+658)
03-13 13:19:33.180 14267 14267 I DEBUG : #03 pc 0006271f /system/lib/libstagefright.so (android::MPEG4Extractor::parseChunk(long long*, int)+658)
03-13 13:19:33.180 14267 14267 I DEBUG : #04 pc 0006271f /system/lib/libstagefright.so (android::MPEG4Extractor::parseChunk(long long*, int)+658)
03-13 13:19:33.180 14267 14267 I DEBUG : #05 pc 0006271f /system/lib/libstagefright.so (android::MPEG4Extractor::parseChunk(long long*, int)+658)
03-13 13:19:33.180 14267 14267 I DEBUG : #06 pc 0006271f /system/lib/libstagefright.so (android::MPEG4Extractor::parseChunk(long long*, int)+658)
03-13 13:19:33.180 14267 14267 I DEBUG : #07 pc 00063aaf /system/lib/libstagefright.so (android::MPEG4Extractor::readMetaData()+46)
03-13 13:19:33.180 14267 14267 I DEBUG : #08 pc 00063d6d /system/lib/libstagefright.so (android::MPEG4Extractor::getMetaData()+8)
03-13 13:19:33.180 14267 14267 I DEBUG : #09 pc 0007f695 /system/lib/libstagefright.so (android::StagefrightMetadataRetriever::getFrameAtTime(long long, int)+28)
03-13 13:19:33.180 14267 14267 I DEBUG : #10 pc 00035945 /system/lib/libmediaplayerservice.so (android::MetadataRetrieverClient::getFrameAtTime(long long, int)+64)
...

查看堆栈调用,最后崩溃在:android::SampleTable::setSampleToChunkParams(long long, unsigned int)+137,非法地址41d00010,该值与R0寄存器值相等,猜想对该寄存器操作导致。
ida反汇编libstagefright.so定位到该行代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.text:0007DD2C loc_7DD2C                               ; CODE XREF: android::SampleTable::setSampleToChunkParams(long long,uint)+BCj
.text:0007DD2C LDR R0, [R6,#8]
.text:0007DD2E MOV.W R10, #8
.text:0007DD32 MUL.W R7, R8, R5
.text:0007DD36 LDRD.W R2, R3, [R6,#0x20]
.text:0007DD3A LDR R1, [R0] // crash!!!
.text:0007DD3C MOV.W R11, #0
.text:0007DD40 ADDS.W R10, R10, R2
.text:0007DD44 STMEA.W SP, {R4,R8}
.text:0007DD48 ADC.W R11, R11, R3
.text:0007DD4C ADDS.W R2, R10, R7
.text:0007DD50 ADC.W R3, R11, #0
.text:0007DD54 LDR R1, [R1,#0x1C]
.text:0007DD56 BLX R1 // 跳转可控!!!
.text:0007DD58 CMP R0, #0xC
.text:0007DD5A BEQ loc_7DD5E
.text:0007DD5C B loc_7DD72

0007DD3A处执行了LDR R1, [R0],获取R0寄存器的值,验证了该想法。注意下边BLX R1将跳转到R1执行代码,R1 = [[R0]+0x1C],所以如果R0可控,则可控制代码执行流程。
R0=41d00010,刚好是EXP中对喷的值sp_addr,所以构造好数据即可执行到shellcode中。

对照源码(附带注释):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
status_t SampleTable::setSampleToChunkParams(
        off_t data_offset, size_t data_size) {
    if (mSampleToChunkOffset >= 0) {
        return ERROR_MALFORMED;
    }
 
  // 数据块偏移
    mSampleToChunkOffset = data_offset;
 
  // 小于8说明无数据(头部)
    if (data_size < 8) {
        return ERROR_MALFORMED;
    }
 
  // 获取size大小,前8个字节
    uint8_t header[8];
    if (mDataSource->readAt(
                data_offset, header, sizeof(header)) < (ssize_t)sizeof(header)) {
        return ERROR_IO;
    }
 
  // 高4位是否为0(version,flags)
    if (U32_AT(header) != 0) {
        // Expected version = 0, flags = 0.
        return ERROR_MALFORMED;
    }
 
  // 获取低4位(Number Of Entries)
    mNumSampleToChunkOffsets = U32_AT(&header[4]);
 
  // 检测数据大小是否合法
    if (data_size < 8 + mNumSampleToChunkOffsets * 12) {
        return ERROR_MALFORMED;
    }
 
  // 创建SampleToChunkEntry对象 size=12
    mSampleToChunkEntries =
        new SampleToChunkEntry[mNumSampleToChunkOffsets];
 
  // 循环解析出每个SampleToChunkEntry数据
    for (uint32_t i = 0; i < mNumSampleToChunkOffsets; ++i) {
        uint8_t buffer[12];
        if (mDataSource->readAt(
                    mSampleToChunkOffset + 8 + i * 12, buffer, sizeof(buffer))
                != (ssize_t)sizeof(buffer)) {
            return ERROR_IO;
        }
 
        CHECK(U32_AT(buffer) >= 1);  // chunk index is 1 based in the spec.
 
        // We want the chunk index to be 0-based.
        mSampleToChunkEntries[i].startChunk = U32_AT(buffer) - 1;
        mSampleToChunkEntries[i].samplesPerChunk = U32_AT(&buffer[4]);
        mSampleToChunkEntries[i].chunkDesc = U32_AT(&buffer[8]);
    }
 
    return OK;
}

最后blx r1对应的代码为mDataSource->readAt(),mDataSource类型为sp<DataSource>,readAt函数在MPEG4DataSource中实现,位于虚表指针0x1C处,如下所示:

所以可以得出崩溃时R0为mDataSource指针,即堆喷时覆盖了mDataSource指针。

再看setSampleToChunkParams函数调用来源:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
status_t MPEG4Extractor::parseChunk(off64_t *offset, int depth) {
...
case FOURCC('s', 't', 's', 'c'):
{
status_t err =
mLastTrack->sampleTable->setSampleToChunkParams(
data_offset, chunk_data_size);

if (err != OK) {
return err;
}

*offset += chunk_size;
break;
}
...
}

在解析stsc类型的数据中调用了该函数,查看sampleTable类,在其+8处刚好为setSampleToChunkParams函数,对照汇编最开始的LDR R0, [R6,#8],可知此时R6为sampleTable指针。

查看exp中stsc数据块内容

再结合前边源码setSampleToChunkParams解析过程:

1.检查数据块大小是否合法,即大于头部8个字节。

2.读取的mNumSampleToChunkOffsets为0xc0000003,经过mul计算后0xc0000003*0c=0x900000024,由于32位乘法指令导致上溢位0x24。
data_size为数据类型stsc前四个字节再减去8个字节的头部,即为0x34-0x8=0x2c,即通过了data_size < 8 + mNumSampleToChunkOffsets * 12的判断。

3.创建0xc0000003个SampleToChunkEntry对象,每个大小为12字节。

4.循环解析每个SampleToChunkEntry数据,由于EXP中stsc数据大小为0x1200字节,去掉前8个字节,则实际循环次数为(0x1200 / 0xc) - 1 = 0x17F次,如果在循环期间能够覆盖sampleTable或者mDataSource则必然崩溃。溢出的数据会不断填充高地址,所以要保证new出来的SampleToChunkEntry地址比他们低。

ida挂载调试,直接下断到
mSampleToChunkEntries = new SampleToChunkEntry[mNumSampleToChunkOffsets].
最后可以看到三个地址如下:

加上在上级调用看到的mLastTrack地址,四个地址如下所示:

1
2
3
4
mSampleToChunkEntries       = 0xb7fb5358
mLastTrack = 0xb7fb59d0
sampleTable = 0xb7fb5a98
mDataSource = 0xb7fb50a8

0xb7fb5a98 - 0xb7fb5358 = 0x740,所以只要溢出超过0x740字节就必定能够覆盖sampleTable地址上的内容,因此LDR R0, [R6,#8]后R0获取的并不是mDataSource而是stsc中的数据。

但是如果只是覆盖了sampleTable地址,执行不到下边的blx r1就会发生崩溃,所以这种情况并不能控制代码执行流程。
在EXP中,通过覆盖Track对象的sampleTable指针,然后程序执行到Track对象的析构函数来控制流程。

在调试中可以知道几个重要结构的地址分布:mSampleToChunkEntries < Track < sampleTable,看下Track结构:

1
2
3
4
5
6
7
8
struct Track {
Track *next;
sp<MetaData> meta;
uint32_t timescale;
sp<SampleTable> sampleTable;
bool includes_expensive_metadata;
bool skipTrack;
};

其中sampleTable指针位于Track+0xC处,假如溢出覆盖到的地址为fake_addr,如果Track+0xC < fake_addr < sampleTable,那么就能覆盖掉Track对象的sampleTable指针并不引起崩溃。
当所有数据块数据解析完后,会调用MPEG4Extractor中的析构函数,遍历track链表执行delete操作。

delete函数会调用android::RefBase::decStrong

此时r0为sampleTable指针,利用方式和cve-2014-7911相同。

可利用条件

利用条件需要多次调试来确定,调试过程几个重要的地方:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// sony xperia S android 4.04
// 关闭aslr
echo 0 > proc/sys/kernel/randomize_va_space

// 断点
trak:
/systembbstagefright.so+0x6CB68 | _ZN7android14MPEG4Extractor10parseChunkEPxi+0x208

stsc:
_ZN7android11SampleTable22setSampleToChunkParamsExj
// (mSampleToChunkEntries = new SampleToChunkEntry[mNumSampleToChunkOffsets];)
_ZN7android11SampleTable22setSampleToChunkParamsExj+0x66

MPEG4Extractor::~MPEG4Extractor
_ZN7android14MPEG4ExtractorD1Ev

decStrong:
_ZNK7android7RefBase9decStrongEPKv

漏洞利用条件限制要几个重要地址:
track1track2sampleTable2SampleToChunkEntry2
MPEG4Extractor在析构是通过track链表遍历逐个删除,那么覆盖掉track1或者track2的sampleTable指针都可以控制程序流程。
为保证程序不崩,溢出的地址必须位于track2和sampleTable2中间,所以可利用条件为:

1
SampleToChunkEntry2 + overflowSize < (track1 | track2) + 0xC < sampleTable2

上边条件需要多次调试来调整overflowSize大小。

比如某次调试中发现:
track1 = 0x2D3D8
track2 = 0x2E458
SampleToChunkEntry2 = 0x2DB00
sampleTable2 = 0x2F010

sampleTable2 - SampleToChunkEntry2 = 0x1510
track2 - SampleToChunkEntry2 = 0x958

满足利用条件,即SampleToChunkEntry2溢出以后能够成功覆盖track2并且程序不崩溃。

另外exp中的堆喷地址sp_addr需要调试去确定一个概率比较高的地址,libc.so:restore_core_regs地址也需要修改。

exp在实际测试中成功率很低,原因都是由于溢出数据覆盖掉了sampleTable地址的数据,导致调用mDataSorce时崩溃,或者覆盖的地址高于track和sampleTable几个对象以外的其他数据,导致应用解析失败或者crash。

另一篇分析文章一步一步调通stagefright exploit说明的tx3g位置问题我在调试时也验证了,SampleToChunkEntry2地址位于track1和track2之间的几率相对来说比较大,但是还是有几率落在track1之前,溢出大小我的机器上调试过程中,在能成功率用的内存布局情况下,track2和SampleToChunkEntry2的距离基本是在0x900~0x1200之间,所以原作者的溢出值并没有问题,需要针对不同机器调试确认,下边是我实际调试中选取的10组数据。

index track1 track2 SampleToChunkEntry2 sampleTable2 overflow_size is_exploit
0 0x2d2d8 0x2d298 0x2e100 0x2e9f0 x no
1 0x2d698 0x15e00 0x2db70 0x2ede0 x no
2 0x40cb0 0x407f0 0x3ef98 0x44528 0x1858 yes
3 0x2d288 0x2d360 0x2d760 0x2ec70 x no
4 0x3d500 0x400a0 0x42e48 0x3fa00 x no
5 0x40a38 0x3ccf0 0x428a8 0x3e530 x no
6 0x2d3d8 0x2e458 0x2db00 0x2f010 0x958 yes
7 0x418e0 0x3feb8 0x43f38 0x3fb90 x no
8 0x3d548 0x410f0 0x3f890 0x3fa88 x no
9 0x3cf78 0x3f690 0x3e5e0 0x42a40 0x10b0 yes

漏洞利用

析构函数执行过后会跳到ROP中,rop布局好数据后先是调用mprotect函数将sp_addr地址长度为0x1000改为可执行权限,然后跳入shell_reverse_tcp代码中执行,参考CVE-2015-1538漏洞利用中的Shellcode分析

这个过程中所有函数的调用通过SVC指令完成,类似于Intel CPU中的int 0x80中断,SVC指令会根据相应的调用号去执行相应的函数,这些编号定义在include\arm\unistd.h中。

1
2
3
4
5
6
7
8
9
10
11
12
#define __NR_restart_syscall (__NR_SYSCALL_BASE+ 0)
#define __NR_exit (__NR_SYSCALL_BASE+ 1)
#define __NR_fork (__NR_SYSCALL_BASE+ 2)
#define __NR_read (__NR_SYSCALL_BASE+ 3)
#define __NR_write (__NR_SYSCALL_BASE+ 4)
#define __NR_open (__NR_SYSCALL_BASE+ 5)
#define __NR_close (__NR_SYSCALL_BASE+ 6)
#define __NR_creat (__NR_SYSCALL_BASE+ 8)
#define __NR_link (__NR_SYSCALL_BASE+ 9)
#define __NR_unlink (__NR_SYSCALL_BASE+ 10)
#define __NR_execve (__NR_SYSCALL_BASE+ 11)
#define __NR_chdir (__NR_SYSCALL_BASE+ 12)

影响

/system/core/rootdir/init.rc定义了mediaserver用户组权限

1
2
3
4
5
service media /system/bin/mediaserver 511    
class main
user media
group audio camera inet net_bt net_bt_admin net_bw_acct drmrpc mediadrm
ioprio rt 4

system/core/include/private/android_filesystem_config.h
所以可以访问audio camera bluetooth。。。

总结

  1. EXP利用MP4格式标准的其它Box申请了一些零碎的内存块创造内存间隙,在程序执行过程中这些零碎内存块很有可能被free,后面申请SampleToChunkEntry的内存就有可能出现在低地址,成功溢出到sampleTable指针并不造成崩溃。
  2. Heap Spray,通过tx3g Box堆喷2M数据,使其某一page出现在预测地址上。
  3. ROP和shell_reverse_tcp中的SVC调用,mprotect函数绕过dep保护。
  4. mp4文件格式解析流程。
  5. 几个重要的内存地址的不确定性导致漏洞无法100%成功,并且rop中重要跳转指令直接硬编码,无法绕过aslr,但是溢出数据足够大能100%造成崩溃。
  6. 或者有其他细节我没理解到,又或者有更好的方法去布局内存分布,值得思考。

cve-2015-3864

cve-2015-3864是同系列中的另外一个漏洞,详见cve-2015-3864