[Format] MP4

[Format] MP4

0x00. Intro

Stagefright는 Android에서 사용하는 미디어 라이브러리이다. 해당 라이브러리는 Android에서 MP3, MP4등 미디어 파일 재생시 사용되는데 악성 MP3, MP4를 분석하는 과정에서 발생하는 취약점에 의해 RCE(Remote Code Execution)이 가능한 취약점을 통틀어 Stagefright 취약점이라 한다. 또한 이와 관련해 CVE-2015-1538 등 CVE 번호 기준 8개의 취약점을 Stagefright v1.0 취약점이라 칭한다. 그런데 최근 Stagefright v2.0이 발견되어 다시 Stagefright 취약점이 이슈가 되고 있다.

따라서 Stagefright 라이브러리 취약점을 언급하기 이전에 미디어 파일인 MP3/MP4 파일 포맷을 먼저 확인해 보자.

0x01. 기본 정보

( 참조 : Tansee - What is MP4 Format Video? )

MP4 파일 포맷은 ISO 미디어 파일 포맷을 기반으로 작성되었으며 MP4 이외에 다음과 같은 파일 포맷 또한 ISO 미디어 파일 포맷을 기반으로 한다.

  • MP3/MP4
  • JPEG2000
  • AVC
  • QT
  • mov
  • 3gp, 3g2

0x10. 전체 구조

ISO 미디어 파일 포맷은 기본적으로 "Box" (또는 atom) 단위로 구성되어 있으며 Big-Endian으로 작성되어 있다. 따라서 이러한 특징을 기준으로 코드를 작성하면 다음과 같다.

In [13]:
class Box:
    def __init__(self):
        self.SIZE = 0
        
    def parseBox(self, *args, **kwargs):
        data = kwargs["data"]
        offset = kwargs["offset"]
        parent = kwargs["parent"]
        
        MEMBER_NAME = ("size type data")
        MEMBER_PATTERN = ">1l4s%ds" % (read32(data, offset) - 8)
        size = MapSize(MEMBER_PATTERN)
        self.SIZE += size
        out = Map("tmp", data[offset:offset + size], MEMBER_NAME, MEMBER_PATTERN)
        
        name = "".join([c for c in out.type if (c.isalpha() == True) or (c.isdigit() == True)])
        out = out._replace(type=name)
        out = addNamedtuple(out.type, out, "offset", offset)
        out = addNamedtuple(out.type, out, "parent", parent)
        return out, offset + self.SIZE
In [14]:
class MP4:
    def __init__(self):
        self.depth = 0
        self.mp4Info = {}
        
    def printBox(self, out):
        offset = "[0x%08x] " % out.offset
        size   = "[%08d] " % out.size
        depth = " " * self.depth * 2
        buffer = offset + size + depth
        if self.depth == 0:
            buffer += "[+]"
        else:
            buffer += "-"

        buffer += " %s" % out.type
        print buffer
        
    def recursive(self, data, absOffset=0, parent=""):
        out = None
        offset = 0
        while offset < len(data):
            obj = Box()
            out, offset = obj.parseBox(data=data, offset=offset, parent=parent)
            out = out._replace(offset=absOffset + out.offset)
            self.mp4Info[out.type] = out
            self.printBox(out)

            if len(out.data) >= 16:
                name_size = 4
                for name_pos in [4, 8, 12]:
                    name = readBinary(out.data, name_pos, name_size)
                    if name in ["mvhd", "iods", "trak", "mdia", "mdhd", "hdlr", "minf", "smhd", "dinf", "dref", "url ", "stbl", "stsd", 
                                "mp4a", "esds", "stts", "stsz", "stsc", "stco", "ctts", "udta", "meta", "tkhd"]:
                        self.depth += 2
                        parent += "/%s" % out.type
                        tmp = self.recursive(out.data[name_pos -4:], out.offset + name_pos + name_size, parent)
                        self.depth -= 2
                        out = addNamedtuple("%s" % out.type, out, tmp.type, tmp)
                        break

        return out
    
    def parseMP4(self, fullname):
        mp4 = file(fullname, "rb").read()
        out = self.recursive(mp4)
        return self.mp4Info
In [15]:
from pprint import pprint
from struct import unpack, calcsize
from collections import namedtuple

import os

def read32(data, offset):
    return unpack(">l", data[offset:offset + 4])[0]

def readBinary(data, offset, size):
    return data[offset:offset + size]

def readBinaryEx(data, offset, size):
    return readBinary(data, offset, size), offset + size

def MapSize(member_pattern):
    return calcsize(member_pattern)

def Map(struct_name, buffer, member_name, member_pattern):
    return namedtuple(struct_name, member_name)._make(unpack(member_pattern, buffer))

def addNamedtuple(new_name, namedtuple1, add_member_name, add_member_value):
    return namedtuple(new_name, namedtuple1._fields+(add_member_name, ))(*(namedtuple1 + (add_member_value,)))

def mergeNamedtuple(new_name, namedtuple1, namedtuple2):
    return namedtuple(new_name, namedtuple1._fields + namedtuple2._fields)(*(namedtuple1 + namedtuple2))
In [16]:
fdir = r"C:\Users\amanaksu\Dropbox\Development\Format\sample"
fname = "1.mp4_"
fullname = os.path.join(fdir, fname)


print "================================================================"
print "File Offset  Box Size   Box Type"
print "----------------------------------------------------------------"
obj = MP4()
mp4Info = obj.parseMP4(fullname)
print "================================================================"
================================================================
File Offset  Box Size   Box Type
----------------------------------------------------------------
[0x00000000] [00000024] [+] ftyp
[0x00000018] [00002400] [+] mdat
[0x00000978] [00000823] [+] moov
[0x00000980] [00000108]     - mvhd
[0x000009ec] [00000024]     - iods
[0x00000a04] [00000564]     - trak
[0x00000a0c] [00000092]         - tkhd
[0x00000a68] [00000464]         - mdia
[0x00000a70] [00000032]             - mdhd
[0x00000a90] [00000033]             - hdlr
[0x00000ab1] [00000391]             - minf
[0x00000ab9] [00000016]                 - smhd
[0x00000ac9] [00000036]                 - dinf
[0x00000ad1] [00000028]                     - dref
[0x00000ae1] [00000012]                         - url
[0x00000aed] [00000331]                 - stbl
[0x00000af5] [00000103]                     - stsd
[0x00000b05] [00000087]                         - mp4a
[0x00000b5c] [00000032]                     - stts
[0x00000b7c] [00000088]                     - stsz
[0x00000bd4] [00000040]                     - stsc
[0x00000bfc] [00000028]                     - stco
[0x00000c18] [00000032]                     - ctts
[0x00000c38] [00000119]     - udta
[0x00000c40] [00000111]         - meta
[0x00000c4c] [00000033]             - hdlr
[0x00000c6d] [00000066]             - ilst
================================================================
In [17]:
class subMP4:
    def __init__(self):
        self.info = None
        
    def __repr__(self, info):
        if self.__call__(info) == False:
            return False
        
        print "[+] %s (offset: %08d, size: %08d)" % (self.info.type, self.info.offset, self.info.size)
        print "  " * 2 + "* Data: ", 
        if len(self.info.data) > 48:
            outData = self.info.data[:48]
        else:
            outData = self.info.data
        pprint(outData)
        
        keys = [k for k in self.info._fields if k not in ["size", "type", "data", "offset"]]
        for k in keys:
            print "  " * 4 + "- %s: %s" % (k, eval("self.info.%s" % k))
        

class ftyp(subMP4):
    def __init__(self):
        subMP4.__init__(self)
        self.info = None
    
    def __call__(self, info):        
        try:
            MEMBER_NAME = ("major_brand major_version")
            MEMBER_PATTERN = ">4s1l"

            offset = 0
            size = MapSize(MEMBER_PATTERN)
            tmp1 = Map("tmp1", info.data[offset:offset + size], MEMBER_NAME, MEMBER_PATTERN)
            offset += size

            comp_brand = []
            comp_brand_size = 4
            while offset < len(info.data):
                val, offset = readBinaryEx(info.data, offset, comp_brand_size)
                comp_brand.append(val)
            tmp2 = addNamedtuple("tmp2", tmp1, "comp_brand", comp_brand)
            self.info = mergeNamedtuple("%s" % info.type, info, tmp2)
            return True
        except:
            return False
In [18]:
def PrintInfo(info):
    return eval("%s().__repr__(info)" % info.type)

PrintInfo(mp4Info["ftyp"])
[+] ftyp (offset: 00000000, size: 00000024)
    * Data: 'mp42\x00\x00\x00\x00mp42isom'
         - parent: 
        - major_brand: mp42
        - major_version: 0
        - comp_brand: ['mp42', 'isom']

0x90. Bug

0x99. 참고

댓글

가장 많이 본 글