File format fuzzing basic

1. 개요

오늘은 퍼징에 대해 이야기 할까 합니다.

퍼징은 소프트웨어의 보안취약점을 찾는 여러가지 방법 중 한가지 입니다.

우선 퍼징을 시작하려면 퍼징 대상 S/W어떻게 퍼징할 것인가에 대한

고민이 필요합니다.

먼저 대상을 정해 봅시다.

이 글에서의 퍼징 대상 S/W는 바로 한글2014 입니다.

그리고 퍼징 방식은 한글 문서의 파일포멧을 어느정도 해석해서

유저가 입력 한 데이터 위주로 퍼징하는 방식을 선택 하겠습니다.

먼저 한글구조에 대해 알아보겠습니다.

2. 한글(hwp)문서의 구조

한글2014에서 지원하는 파일 포멧은 상당히 많습니다.

확장자를 살펴보면 hwp, hwpx, hml, doc, docx 등 익숙한 확장자도 있지만

그렇지 않은 확장자도 존재합니다.

이 중에서 퍼징의 대상은 전통적인 한글 확장자인 .hwp를 대상으로 하겠습니다.

.hwp 파일 포멧에 대해 아주 간략하게 살펴보고 퍼징전략을 세워봅시다.

먼저 hwp파일을 헥스에디터로 열어보면 아래와 같은 모습이 보입니다.

보통 파일의 첫 4 ~ 16바이트는 각각의 파일을 구분하기 위해 파일헤더 라고 부릅니다.

우리가 알고있는 한글파일의 4바이트는 \xd0\xcf\x11\xe0(DOCFILE) 입니다.

이는 OLE의 파일헤더와 일치합니다.

한글을 살펴보기 전에 OLE 구조도 조금 살펴보는게 좋을듯 합니다.

2.1 OLE(Object Linking & Embeding)구조

이름에서 알 수 있듯 어떠한 오브젝트(그림, 동영상, 플래시 등)을

삽입 하거나, 링킹(연결)하는데 사용하는 구조입니다.

전체적인 구조는 아래처럼 생겼습니다!

http://ghostkei.tistory.com/308
ref : http://ghostkei.tistory.com/308

섹터가 등장하는 것을 보면 파일시스템이 생각납니다. OLE 구조 자체는

파일시스템과 비슷하게 구성되어 있습니다.

OLE 구조를 해석하면 아래와 같은 형태가 나타납니다.

http://ghostkei.tistory.com/308
ref : http://ghostkei.tistory.com/308

생김새를 보면 c:\(root) 이하에 디렉토리와 폴더처럼 구성되어 있는 것을 알 수 있습니다.

크게 나눠보면 세가지로 분류 할 수 있습니다.

  • RootStroage
  • Strorage : 스트림을 분류
  • Stream : 실제 파일(이미지, 데이터 등)

위와 같은 OLE구조를 해석하는 도구가 여러가지 존재 하는데

한글문서 구조를 이해하는데 가장 좋은 도구는 HwpScan2 입니다.

설치파일은 http://www.nurilab.com/?p=58 이곳에서 다운로드 받을 수 있습니다.

한번 점검도구로 한글 문서를 열어 볼까요?

아래와 같은 구조를 한눈에 쉽게 볼 수 있을 겁니다.

위에 간략하게 설명한 OLE 구조의 트리형태를 반영해서 스토리지와 스트림으로

분류되어 원하는 곳을 손쉽게 볼 수 있습니다.

2.2 한글의 어느곳을 퍼징해야 할까?

개요에서 우리는 한글문서 중 유저가 입력한 데이터 위주로 퍼징을 하겠다고 전략을 세웠습니다.

그렇다면 실제 한글 문서 구조에서는 어디에 데이터가 저장되는지 알 수 있다면 좋을 듯 합니다.

먼저 한글문서의 공개된 포멧을 살펴봅니다.

위 화면은 한글문서 포멧의 구조를 나타낸 표 입니다.

스토리지와 스트림이 보이죠?

이 중에 우리가 관심을 가져야 할 만한 곳은 두군데 입니다.

  • 본문 (BodyText)
  • 바이너리 데이터(BinData)

본문은 이름 그대로 사용자가 입력한 내용의 정보를 담고 있기 때문에

퍼징 대상으로 선정되어야 할거 같구요, 바이너리 데이터 영역은

한글 문서 내부에 사진과 같은 이미지, 동영상 등의 객체들을 삽입하면

OLE 구조 형태의 스트림으로 BinData영역에 저장되는 곳입니다.

두 곳 모두 사용자의 입력으로 데이터가 변경 가능한 영역입니다.

이제 위 영역을 해석할 수 있는 파이썬 스크립트를 작성해 봅시다.

관련 소스는 https://github.com/cdpython/NewFuzzer 에서 다운로드가 가능합니다.

3. Fuzzer 만들기

먼저 퍼저를 만들기위해 사용한 언어는 python입니다.

그리고 사용한 추가 라이브러리는 아래와 같습니다.

퍼저가 할 동작들을 나열해 보면 아래와 같습니다.

  1. input 파일중에 대상을 선택
  2. 선택한 파일을 분석
  3. 분석한 결과를 바탕으로 바이트 변조
  4. 대상 프로세스 실행
  5. 크래시 여부 확인
  6. 크래시인 경우 로깅 및 파일저장, 1로 이동
  7. 크래시가 아닌 경우 프로세스 종료, 1로 이동

이제 소스코드를 작성해 보도록 하겠습니다.

#-*- coding: utf-8 -*-
"""
    HWP Fuzzer 
    author : cdpython at hacklab.kr

"""
import OleFileIO_PL as OLE
import winappdbg
from winappdbg import *
import os
import subprocess
import shutil
import threading
from datetime import datetime
from random import choice, uniform, sample
import warnings
warnings.filterwarnings("ignore")

class Fuzzer():
    def __init__(self):
        # 퍼징 대상 프로그램 경로
        self.program = r"C:\Program Files\HNC\HOffice9\Bin\Hwp.exe"
        # 뮤테이션 할 파일명
        self.target_file = ""
        # 크래서 저장, seed파일(input)경로, 임시폴더 경로
        self.crashDir = os.getcwd()+os.sep+"crash"+os.sep
        self.seedDir = os.getcwd()+os.sep+"seed"+os.sep
        self.tmp = os.getcwd()+os.sep+"tmp"+os.sep
        self.timer = None
        # 크래시 중복 제거용 리스트
        self.unique_list = []
        # 출력용 카운트
        self.crash_count = 0
        self.iter_count = 0

    # 지정된 디렉토리에서 파일을 선택하고
    # 임시폴더로 복사 하는 함수
    def pick(self, seedDir):
        fname = choice(os.listdir(seedDir))
        shutil.copy(self.seedDir+fname, self.tmp+fname)
        return self.tmp+fname

    # 타이머에서 종료 시 사용하는 함수    
    def quit(self, proc):
        self.timer=False
        try:
            proc.kill()
        except:
            subprocess.call("taskkill /f /im %s" % self.program.split("\\")[-1] ,stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
    # 디버거 핸들러
    # 크래시 모니터링, 중복 크래시 체크, 크래시 파일 및 로그 저
    def handle(self, event):
        if event.get_event_code() == win32.EXCEPTION_DEBUG_EVENT and event.is_last_chance():
            self.timer.cancel()
            self.timer = False

            crash = Crash(event)
            crash.fetch_extra_data( event, takeMemorySnapshot = 0 ) 
            unique = crash.signature[3]
            if unique  in self.unique_list:
                print "[-] Duplicate Crash"
                try:
                    event.get_process().kill()
                except:
                    subprocess.call("taskkill /f /im %s" % self.program.split("\\")[-1] ,stdout=subprocess.PIPE, stderr=subprocess.PIPE, shell=True)
            else:
                self.crash_count += 1
                self.unique_list.append(unique)
                try:
                    with open('unique.txt','w') as f:
                        f.write("\n".join(self.unique_list))
                except:
                    import traceback
                    print traceback.format_exc()
                    pass
                try:
                    report = crash.fullReport()
                    prog = self.program.split("\\")[-1]

                    with open(self.crashDir+prog+'_'+datetime.now().strftime("%y-%m-%d-%H-%M")+'_Crash'+".log", "w") as f:
                        f.write(report)
                    shutil.copy(self.target_file, self.crashDir+prog+'_'\
                        +datetime.now().strftime("%y-%m-%d-%H-%M")+'_Crash.'+self.target_file.split(".")[-1])
                except:
                    import traceback
                    print traceback.format_exc()
                    pass
                try:
                    event.get_process().kill()
                except:
                    subprocess.call("taskkill /f /im %s" % self.program.split("\\")[-1])

        else:
            if not self.timer:
                self.timer = threading.Timer(2, self.quit, [event.get_process()])
                self.timer.start()
    # HWP 파일 변조      
    def mutate(self):
        mutateBytes = xrange(256)
        fuzzing_list = []
        fuzz_offset = []
        mutate_position = []
        # 파일을 OLE구조로 해석
        ole = OLE.OleFileIO(self.target_file)
        # OLE구조 리스트 저장
        ole_list = ole.listdir()
        # 본문, BinData영역의 상위 16바이트와 해당 스트림의 사이즈 저
        for entry in ole_list:
            if "BodyText" in entry:
                sections = entry[1:]
                for sec in  sections:
                    stream = entry[0]+"/"+sec
                    size = ole.get_size(stream)
                    fuzzing_list.append( (ole.openstream(stream).read(16), size) )
            if "BinData" in entry:
                sections = entry[1:]
                for sec in  sections:
                    stream = entry[0]+"/"+sec
                    size = ole.get_size(stream)
                    fuzzing_list.append( (ole.openstream(stream).read(16), size) )

        ole.close()
        # 변조할 파일을 열고 버퍼로 저장
        with open(self.target_file, 'rb') as f:
            buf = f.read()
        # 파일에서 해당 스트림(BodyText, Bindata)의 offset을 검색
        for magic, size in fuzzing_list:
            if buf.find(magic) != -1:
                offset = buf.find(magic)
                mutate_position.append((offset, size))
        # 해당 스트림의 파일 offset에서 스트림 사이즈의 1~3% 오프셋을 랜덤하게 선택        
        for offset, size in mutate_position:
            fuzz_offset += sample(xrange(offset, offset+size), int(size*uniform(0.001, 0.03)))

        arr = bytearray(buf)
        flen = len(buf)
        # 위에서 랜덤하게 선택한 오프셋의 값을 0x00~0xff 중 하나로 변조
        for index in fuzz_offset:
            arr[index] = choice(mutateBytes)
        # 파일로 저장    
        with open(self.target_file, 'wb') as f:
            f.write(arr)        

    # 뮤테이션 후 남은 파일 제거
    def remove(self):
        while len(os.listdir("tmp")) != 0 :
            for x in os.listdir("tmp"):
                try:
                    os.remove(r"tmp\%s" % x)
                except:
                    pass
    # 퍼징 메인 루틴
    def start(self):
        while True:
            self.iter_count += 1
            self.target_file = self.pick(self.seedDir)
            self.mutate()
            print "iter : ", self.iter_count, "crash : ", self.crash_count
            dbg=Debug(self.handle, bKillOnExit=True)
            dbg.execl('"%s" "%s"' % (self.program, self.target_file))
            dbg.loop()
            self.remove()

if __name__ == '__main__' :
    fuzz = Fuzzer()
    fuzz.start()

4. 앞으로 해 볼만한 일들

위에서 작성한 퍼저는 한글문서의 전반적인 구조 내에서

사용자가 입력 한 데이터를 퍼징하도록 동작합니다.

하지만 한글 문서 포멧을 참고해서 본문영역(BodyText)에 저장된 스트림의

의미를 파싱하고 또 한번 데이터만 퍼징하는 방법도 효율적일거라 생각합니다.

또한 BinData 영역에 저장된 OLE 스트림도 파싱이 가능한 형태라면

분석해서 퍼징하는 방식도 가능하리라 생각합니다.

5. 마치며

끝으로 퍼징에 대해 처음 공부할 때 많은 조언 해주신 passket형님께 감사드리며

OLE 구조에 대해 훌륭한 강의로 좋은자료 작성해 주신

최원혁 누리랩 대표님 역시 감사드립니다.

작성한 자료의 오탈자, 피드백, 문의사항은 댓글로 달아주시면

제가 아는 범위내에서 답변 드리도록 하겠습니다.

참고자료:
http://www.nurilab.com/?p=114
http://ghostkei.tistory.com/308

One thought on “File format fuzzing basic”

Leave a Reply

Your email address will not be published. Required fields are marked *