개요
OLE 파일은 각종 워드 문서에서 쓰이는 파일 포맷으로 FAT 파일 시스템과 유사하게 스토리지와 스트림 으로 구성돼있다. 과거부터 한글 워드프로세서나 MS Office 취약점을 공격하는 악성코드들로 인해 문서 악성코드를 접하는 사람이라면 반드시 알아둬야 하는 파일 포맷이기도 하다. 최신 office 파일 포맷은 OOXML 을 사용해 그 중요도가 과거에 비해 많이 떨어졌지만 OOXML 내부에서도 여전히 OLE 파일이 이용되고 있고, OLE 포맷의 워드파일 지원이 완전히 끝난것이 아니기에 여전히 알아두면 좋은 지식이라 생각된다.
목적
MS에서 OLE 파일 구조를 공개하기 전, LAOLA 프로젝트를 기반으로 한 분석이 주를 이뤄 현재 MS 에서 공개한 용어와 혼선이 많은 것이 사실이다. 이 글에선 MS-CFB 스펙문서를 기반으로 OLE 파일 포맷을 분석하여 스토리지와 스트림을 분리하는데 촛점을 두고, LAOLA 프로젝트 용어와 MS에서 발표한 용어의 차이에 대해서도 함께 다뤄보겠다.
기본개념
CFB 는 OLE 와 동의어로 봐도 좋다. 줄여서 compound file 이라고도 한다. 실제로 MS-CFB 문서의 이름과 그 내용에서 OLE 란 용어보단 컴파운드 파일이란 용어를 사용한다.
시작은 전통적인 파일시스템의 한계로 인해 하나의 파일 내에 파일과 폴더 구조를 넣기 위한 니즈에서 출발했다. 여러 요소(파일, 폴더) 가 하나의 파일(Compoud File Binary) 안에 흩어져 저장되어 있기에 이런 요소들을 하나의 스토리지, 하나의 스트림 으로 분리하는 작업이 분석의 첫걸음 이라 볼 수 있다.
스토리지와 스트림
OLE 파일은 일반적인 파일시스템과 같다고 생각하면 된다. 폴더 역할의 스토리지(Storage)와 파일 역할의 스트림(Stream)으로 구성돼있다. 그리고 OLE 파일 내에 스토리지와 스트림을 저장하기 위해 FAT 파일시스템과 매우 유사한 방식을 취한다. FAT 파일시스템에 대한 기본적인 지식이 있다면 더욱 쉽게 접근할 수 있다. 모든 정보는 512 byte 의 섹터 단위로 저장되어있다.1
CFB 구성요소
compound file 을 이해하기 위해 파일을 이루는 여러 구성요소를 알아둘 필요가 있다. 아래 그림은 MS-CFB 문서에 나온 컴파운드 파일의 구조이다.
Compound File
Compound File, OLE 파일, 파일 그 자체를 말한다. 파일을 파싱해 들어가면 그림과 같은 구조를 살펴볼 수 있다.
DIFAT Array (Extra BBAT)
일반적으로 크기가 작은 OLE 파일(6.875 MB 이하) 에는 존재하지 않는 배열이다. LAOLA 용어로 Extra BBAT 와 동일하다. FAT Array 와 동일하게 사용한다. 이후 자세히 설명한다.
FAT Array (BBAT)
FAT Array 는 그 단어처럼 32-bit 정수로 이뤄진 배열이다. 배열의 인덱스는 섹터번호를 말하고, 인덱스에 해당하는 원소는 다음 인덱스 번호를 말한다. LAOLA 용어로 BBAT 라고한다. 하나의 완전한 스트림을 만들기 위해 섹터번호들이 써있는 지도 정도로 생각하면 된다.
큰 파일이 디스크에 처음부터 끝까지 한 줄로 이어져 기록되지 않는 것처럼, OLE 파일에서도 스트림을 한 줄로 이어 저장하지 않는다. 저장은 512 byte 크기의 섹터(Sector) 단위로 나눠 하게 되는데, 하나의 스트림이 각기 몇 번 섹터에 나뉘어 저장되어 있는지 알기 위해 참조하는 테이블(배열)이다. FAT 그 자체로도 부르지만 이 글에선 구분을 쉽게 하기위해 FAT Array 란 용어로 통일한다. 실제로 과거 FAT File System 을 공부하며 기록한 자료를 살펴보니 OLE 구조가 얼마나 FAT File System과 유사한지 알 수 있었다.
Directory Entry Array
스토리지·스트림에 관련된 메타정보(위치, 크기, 이름 등)가 저장된 곳을 Directory Entry 라고 한다. Directory Entry Array 는 그 이름과 같이 이 Directory Entry 의 배열을 말한다. LAOLA 에선 Property 로 불린다. 메타정보가 있기에 Directory Entry 정보만 갖고 있으면, 윈도우 탐색기와 같은 스토리지·스트림의 트리구조를 그릴 수 있다.
이 역시 FAT File System 의 그것과 동일한 역할을 한다.
0x80 의 길이를 갖는 Directory Entry 는 성격에 따라 세 가지로 나눌 수 있다. Stream, Storage 그리고 Root 가 그것이다. 스토리지·스트림과 별개로 최상위 엔트리를 뜻하는 Root Directory Entry 는 다른 Directory Entry 와 동일한 구조체 크기를 갖지만 조금 특별한 용도를 갖고 있다. 이후 자세히 설명한다.
mini FAT Array
스트림의 크기가 섹터단위(512 byte)로 고정되기에 저장공간의 낭비가 발생한다. 이를 방지하기 위해 mini FAT 의 개념이 있다. mini FAT은 64 byte 의 mini sector 단위로 데이터에 접근한다. 짧은 길이의 스트림만을 대상으로 하지만 스트림이 저장된 위치를 설명한다는 점에서 FAT Array 와 그 역할이 동일하다. LAOLA 에선 SBAT 로 불린다.
파싱
앞서 대략적으로 살펴본 내용을 파이썬을 통해 파싱해보도록 하겠다. 먼저 CFB 파일의 특정 섹터의 데이터를 가져오는 readSector(sectorNum)
함수에 대한 이해가 필요하다. 아래는 OleParser 클래스의 메쏘드 readSector()
의 소스이다.
def readSector(self, sectorNum):
"""
섹터 번호에 해당하는 값을 파일에서 읽어온다
:param sectorNum:
:return:
"""
offset = (sectorNum + 1) * SECTORSIZE
self.oleFile.seek(offset)
buf = self.oleFile.read(SECTORSIZE)
return buf
CFB 는 FAT 파일시스템과 같이 모든 데이터에 섹터단위로 접근한다. 주의할 점은 Sector의 0번 인덱스 라고 한다면 CFB 파일의 첫 시작위치부터 512 byte 라고 생각할 수 있는데, offset = (sectorNum + 1) * SECTORSIZE
부분을 본다면 원하는 섹터에 1을 더해서 읽어오는 것을 볼 수 있다. 이는 CFB 의 섹터는 -1 부터 시작하기 때문이다. 그리고 이 -1 번 섹터가 바로 CompoundFileHeader 구조체이다.
앞으로 파싱은 MS-CFB 에 명시된 구조체 원형을 이용해 진행한다. 여기서 파이썬에서 구조체로 데이터를 가져오는 과정에서 byte padding 이 일어날 수 있으니 주의를 요한다.
CompoundFileHeader 얻기
-1 번 섹터에 해당하는 부분이다. LAOLA 에서는 헤더블록으로 불린다. 파이썬 코드로 보면 아래와 같이 구조체를 선언할 수 있다.
CompoundFileHeader 구조체 원형
MAXFATENTRY = 109
class CompoundFileHeader(LittleEndianStructure):
_fields_ = [
('HeaderSignature', c_ubyte * 8),
('HeaderCLSID', c_ubyte * 16),
('MinorVersion', c_ushort), # SHOULD be 0x003E if Major is 0x0003 or 0x0004
('MajorVersion', c_ushort),
('ByteOrder', c_ushort), # MUST be set to 0xFFFE
('SectorShift', c_ushort), # MUST be set to 0x0009, or 0x000c, depending of MajorVersion
('MiniSectorShift', c_ushort),
('Reserved', c_ubyte * 6), # MUST 0
('NumberOfDirectorySectors', c_uint32),
('NumberOfFATSectors', c_uint32),
('FirstDirectorySectorLocation', c_uint32),
('TransactionSignatureNumber', c_uint32),
('MiniStreamCutoffSize', c_uint32),
('FirstMiniFATSectorLocation', c_uint32),
('NumberOfMiniFATSectors', c_uint32),
('FirstDIFATSectorLocation ', c_uint32),
('NumberOfDIFATSectors', c_uint32),
('DIFAT', c_uint32 * MAXFATENTRY)
]
pattern = '< 8s 16s H H H H H 6s I I I I I I I I I ' + str(MAXFATENTRY) + 'I'
pattern = '< 8s 16s H H H H H 6s I I I I I I I I I ' + str(MAXFATENTRY) + 'I'
을 선언 해 struct 모듈의 unpack 에서 손쉽게 이용하고자 했다. 하지만 unpack 함수에는 큰 문제가 있다. 리턴을 반드시 튜플로 한다는 것과, 모든 패턴원소를 각기 다른 튜플의 원소로 리턴한다는 것이 그것이다. 이를태면 <8s
란 패턴은 원소가 8개인 튜플을 리턴할것이다. 따라서 unpack 을 이용할 경우 백수십개에 이르는 CompoundFileHeader 원소를 일일히 손으로 대입해줘야 한다.
따라서 구조체로 값을 넣을 때 unpack 을 이용하기보다 아래의 make(self, bytes)
함수를 선언하길 권한다. 파이썬 구조체(클래스) 별로 하나씩 선언해 둔다면, 구조체의 크기에 관계없이 값을 읽어올 수 있다. 또한 _pack_ = 1
을 선언해 둠으로써 byte padding 을 예방할 수 있다.
편한 파이썬 구조체 사용의 예
class MyStructure(LittleEndianStructure):
_pack_ = 1
_fields_ = [
('MemberOne', c_uint32 * 0x10),
('MemberTwo', c_ubyte * 0x20)
]
def make(self, bytes):
fit = min(len(bytes), sizeof(self))
memmove(addressof(self), bytes, fit)
FAT Array 얻기
CompoundFileHeader 구조체를 완성했으니 이를 토대로 FAT Array 를 만들어 보겠다. 설명에 앞서, FAT Array 가 이미 있다는 가정하에 FAT Array 를 통해 데이터를 접근하는 예를 들어본다.
- 위치를 알 수 없는 특정 데이터(스트림)가 OLE 파일상 10번, 13번, 14번 섹터에 나뉘어 저장되어 있다.
- 특정 구조체에 명시된 값을 통해 이 데이터의 시작 섹터가 10이라는 사실을 알았다.
- OLE 파일의 10번 섹터를 읽는다
- 다음 섹터 번호를 얻기 위해 FAT Array 의 10번 인덱스에 접근한다
- 10번 인덱스에 해당하는 값인 13을 얻는다
- OLE 파일의 13번 섹터를 읽는다
- 위의 과정을 반복해 FAT Array 의 14번 인덱스에 해당하는 값에서 ENDOFCHAIN(==0xFFFFFFFE) 을 만나면 종료한다
이와 같이 접근하는 구조를 체인 구조를 갖는다고도 한다. 이 과정을 구현한 코드는 아래와 같다. self.FATArray 는 이미 확보돼있다고 가정한다.
def readStreamFromFATarray(self, startSector):
"""
FAT array 체인에 따라 해당하는 스트림 조각 들을 하나로 리턴한다
:param startSector: 시작엔트리
:return: b''
"""
buf = b''
sectorNum = startSector
while sectorNum not in (ENDOFCHAIN, FREESECT):
buf = buf + self.readSector(sectorNum)
sectorNum = self.FATarray[sectorNum]
return buf
OLE 파일의 모든 데이터(스트림)은 FAT Array 를 참고하여 접근할 수 있다고 설명한 바 있다. 그렇다면 FAT Array 없인 어떤 데이터에도 접근할 수 없을텐데 FAT Array 그 자체는 어디에 있는 것일까? 여기에 대한 해결책이 FAT Sector 이다.2
FAT Array 는 여타 다른 데이터와 마찬가지로 OLE(CFB) 파일 내에 섹터단위로 흩어져 있다. 그리고 FAT Array 의 일부가 있는 섹터를 FAT Sector 라고 한다. FAT Sector 들의 위치는 CompoundFileHeader 의 멤버 DIFAT 가 담당한다. FAT Array 를 만들어야 되는데 DIFAT 를 참조한다니 혼동이 올 수 있는데 지금부터 그 이유를 설명한다.
앞서 말한 바와 같이 일반적으로 크기가 작은 CFB 파일(6.875 MB 이하) 에서 DIFAT Array 는 존재하지 않는다. 이 경우에 CompoundFileHeader 의 각 멤버에 들어간 값은 아래와 같다.
# DIFAT 가 없는 경우
CompoundFileHeader.NumberOfDIFATSectors == 0
CompoundFileHeader.FirstDIFATSectorLocation == 0xFFFFFFFE
위와 같은 경우, -1 번 섹터의 끝까지 이어지는 109개의 DIFAT 원소를 읽어 FAT Sector 들의 위치를 얻고, 각각의 섹터를 조합하여 FAT Array 를 만들면 된다. 이 경우 DIFAT 멤버는 이름만 DIFAT 이고 FAT 처럼 취급한다 볼 수 있다.
# DIFAT 가 있는 경우
CompoundFileHeader.NumberOfDIFATSectors != 0
CompoundFileHeader.FirstDIFATSectorLocation == FIRST_DIFAT_SECTOR_LOCATION
하지만 DIFAT Array 가 존재할 경우3, CompoundFileHeader.FirstDIFATSectorLocation 를 참조하여, DIFAT Array 를 만들어야 한다. 하지만 아직 FAT Array 처럼 별로도 참조할 지도를 만들기 전이기 때문에 CompoundFileHeader.FirstDIFATSectorLocation 를 통해 찾은 섹터의 마지막 4byte를 다음 섹터 위치를 표시하는데 사용한다. 이를 코드로 표현하면 아래와 같다.
if self.compoundFileHeader.NumberOfDIFATSectors > 0:
DIFATsector = self.compoundFileHeader.FirstDIFATSectorLocation
for i in range(0, self.compoundFileHeader.NumberOfDIFATSectors):
buf += self.readSector(DIFATsector)
DIFATsector, = struct.unpack('<I', buf[-4])
여기서 중요한 점은, 이렇게 찾은 DIFAT 를 FAT Sectors 뒤에 그대로 붙여서 쓴다는 사실이다. 이제 구조체 멤버 DIFAT 가 왜 이름이 이렇게 됐는지 알법하다. DIFAT 들을 통틀어 전체 FAT Sectors 를 구하고, 이를 통해 FAT Array 를 만드는 과정은 아래와 같이 구현할 수 있다.
'''
FAT Array 를 설정한다 (BBAT)
'''
FATsectors = list()
for i in range(0, MAXFATENTRY):
if (self.compoundFileHeader.DIFAT[i] == FREESECT) or \
(self.compoundFileHeader.NumberOfFATSectors < i):
break
FATsectors.append(self.compoundFileHeader.DIFAT[i])
# DIFAT Array 가 존재한다면
buf = b''
DIFATsector = self.compoundFileHeader.FirstDIFATSectorLocation
for i in range(0, self.compoundFileHeader.NumberOfDIFATSectors):
# 해당 섹터를 읽고
buf = self.readSector(DIFATsector)
# 인덱스로 4byte 씩 접근할 수 있게 가공한다
DIFAT = self.littleEndianBinaryToList(buf)
# 다음섹터 위치를 표시하는 마지막 인덱스를 취하고
DIFATsector = DIFAT[-1]
# FATsectors 에는 추가하지 않는다
FATsectors += DIFAT[:-1]
buf = b''
for FAT in FATsectors:
buf += self.readSector(FAT)
# 인덱스로 4byte 씩 접근할 수 있게 가공하면 최종적으로 FAT Array (BBAT)가 완성된다
self.FATarray = self.littleEndianBinaryToList(buf)
DirectoryEntry Array 얻기
DirectoryEntry Array 는 DirectoryEntry 들이 있는 배열이다. FAT Array 가 저장된 것 처럼, DirectoryEntry Sector 에 네 개씩 나뉘어 CFB 파일에 흩어져 있다.4 CompoundFileHeader 구조체의 FirstDirectorySectorLocation 멤버에 통해 체인의 시작지점을 알 수 있으므로, FAT Array 에서 쉽게 DirectoryEntry Array 전체를 얻을 수 있다.
DirectoryEntry 구조체 원형
DirectoryEntry 구조체는 세 종류(스토리지, 스트림, 루트)로 나뉜다고 앞서 설명한 바 있다. 용도는 조금씩 다르지만 구조체의 크기는 동일하므로 동일 구조체를 사용해도 진행에 무리는 없다. 여기서는 Root DirectoryEntry 만 다룬다.
offset | Field Name | Example |
---|---|---|
0x0400 | Directory Entry Name | Root Entry (section 2.6.2) |
0x0440 | Directory Entry Name Length | 0x16 (22 bytes) |
0x0442 | Object Type | 0x05 (root storage) |
0x0443 | Color Flag | 0x01 (black) |
0x0444 | Left Sibling ID | 0xFFFFFFFF (none) |
0x0448 | Right Sibling ID | 0xFFFFFFFF (none) |
0x044C | Child ID | 0x00000001 (Stream ID 1: “Storage 1” (section 2.6.3)) |
0x0450 | CLSID | 0x11CEC15456616700 0xAA005385 0x5BF9A100 |
0x0460 | State Flags | 0x00000000 |
0x0464 | Creation Time | 0x0000000000000000 |
0x046C | Modification Time | 0x0000000000000000 |
0x0474 | Starting Sector Location | 0x00000003 (sector #3 for mini Stream) |
0x0478 | Stream Size | 0x0000000000000240 (576 bytes) |
DirectoryEntry 의 성격을 알고 싶으면 ObjectType 을 확인하면 된다. 상세한 필드값에 대한 설명은 문서를 참고하길 바란다. LEFTSiblingID, RightSiblingID 는 각각 수평방향에 다른 DirectoryEntry 가 있는지 알려준다. Root 의 경우 최상위 디렉토리기에 수직방향(ChildID)에만 값이 존재한다.5
수평·수직 방향의 DirectoryEntry 를 나타냄에 있어 ID 란 용어를 사용한 것이 보인다. 이 ID 는 앞서 확보한 DirectoryEntry Array 의 인덱스 번호이자 해당 DirectoryEntry 의 ID 자체를 나타낸다. 예를들어 Root DirectoryEntry의 경우 항상 0으로 고정이니, DirectoryEntryArray[0] 으로 접근할 수 있고, 그 자식인 “Storage 1” 은 DirectoryEntryArray[1] 로 접근할 수 있다. 물론 배열의 원소의 크기는 sizeof(DirectoryEntry)
이다.
Root DirectoryEntry 는 조금 특별한 역할이 있다. 그것은 miniFAT Array 가 실제 데이터를 갖고오기 위해 사용하는 miniFAT Stream 을 갖고 있단 사실이다. miniFAT Stream 은 LAOLA 에서 Small Data Block 과 동일하다. 이 내용은 miniFAT array 를 먼저 확보한 후 다시 설명한다.
miniFAT Array 얻기
miniFAT Array 의 차례이다. CompoundFileHeader 의 멤버 NumberOfMiniFATSectors 와 FirstMiniFATSectorLocation 를 참고하여 FAT Array 를 만들 때와 같은 방식으로 miniFAT Array 를 만들면 된다. 단, FAT Sector 들이 CFB 파일에서 자체에 있던 것과 달리, miniFAT sector 는 NumberOfMiniFATSectors 값에 따라, CFB 파일 자체에 있을 수도, FAT Array 에 있을 수도 있다.6 글로 쓰면 복잡해 보이지만 코드로 보면 쉽게 이해할 수 있다.
'''
miniFAT Array 를 설정한다 (SBAT)
'''
startSector = self.compoundFileHeader.FirstMiniFATSectorLocation
if self.compoundFileHeader.NumberOfMiniFATSectors > 1: # FATarray 를 참고해 여러 섹터를 가져옴
buf = self.readStreamFromFATarray(startSector)
else: # 파일에서 해당 섹터만 가져옴
buf = self.readSector(startSector)
# 인덱스로 4byte 씩 접근할 수 있게 배열로 저장한다
self.miniFATarray = self.littleEndianBinaryToList(buf)
miniFAT stream 얻기
miniFAT array 는 FAT array 와 달리 MINISECTORSIZE 로 데이터에 접근하기 때문에 FAT Array 처럼 CFB 파일을 바로 읽지 않는다. 따라서 miniFAT Array 가 데이터를 읽기 위한 전용 저장공간이 따로 마련되어 있는데, 이곳을 miniFAT stream 이라 부르고, Root DirectoryEntry 를 통해 얻을 수 있다.
잠시 DirectoryEntry 로 돌아가보자. DirectoryEntry 구조체의 멤버 StartingSectorLocation 와 StreamSize 에 집중할 필요가 있다. DirectoryEntry 의 ObjectType 이 STORAGE 일 경우 두 멤버는 없는 값 이지만, 스트림일 경우, 마치 파일처럼, 스트림의 실제 내용이 있는 위치를 나타내는 멤버이다. 그리고 이 스트림의 크기(StreamSize 멤버)에 따라 바로, 큰 스트림 과 작은 스트림 으로 나뉜다.7
Root DirectoryEntry 는 최상위 DirectoryEntry 인 만큼, 스토리지 DirectoryEntry 처럼 스트림이 없을 거라 생각할 수 있지만, Root DirectoryEntry 는 특수한 역할이 있다. 그것은 miniFAT stream 을 나타내는 것이다.
Root DirectoryEntry 는 반드시 스트림이 존재한다. 이 스트림은 StreamSize 멤버의 크기에 상관없이 FAT Array 를 참조해서 얻어온다8. 이렇게 얻은 Root Directory 의 스트림이 바로 miniFAT stream 이 된다. 즉, miniFAT Array 는 CFB 파일 자체가 아닌 바로 이 miniFAT stream 에서 데이터를 읽어온다.
주의할 점은 miniFAT stream 을 읽을때는 FAT 과 달리 CompoundFileHeader 가 따로 없기에 인덱스+1 해주는 공식을 사용하지 않는다는 점이다. 아래의 코드를 보면 readSector()
와 무엇이 다른지 쉽게 확인할 수 있다.
def readMiniSector(self, sectorNum):
"""
미니 섹터 번호에 해당하는 값을 miniFATstream 에서 읽어온다
파일 자체인 FAT stream 과 달리
miniFAT stream 을 가져올땐 헤더가 없어, offset 을 구하는 공식이 다르다
:param sectorNum:
:return:
"""
offset = sectorNum * MINISECTORSIZE
buf = self.miniFATstream[offset:offset+MINISECTORSIZE]
return buf
큰 스트림과 작은 스트림에 따라 각기 다른 스트림에서 데이터를 읽어오는 함수는 아래와 같이 구현 할 수 있을 것이다.
def readStream(self, startSector, streamSize, objectType):
"""
miniFAT 과 FAT 스트림을 읽어오는 래핑함수
:param startSector:
:param streamSize:
:param objectType:
:return:
"""
if (objectType == STORAGE) or (streamSize == NOSTREAM):
return
if (streamSize < MINISTREAMCUTOFFSIZE) and (objectType != ROOT):
buf = self.readStreamFromMiniFATarray(startSector)
else:
buf = self.readStreamFromFATarray(startSector)
return buf
결론
여기까지 진행했다면 CFB 파일의 각 요소들에 쉽게 접근할 수 있는 발판이 마련된 것이다. 다음부터 할 일은 CFB 파일포맷을 활용하는 다양한 3rd party 프로그램들(한글2003, 오피스2003 등)이 CFB 파일을 어떻게 활용하여 자신만의 기능을 구현하는지 분석하는 일이 남아있다.
이제 시작일 뿐이다
CFB 파일포맷은 XML 이나 OOXML 처럼 다양한 분야에 활용되는 파일 포맷이기에 모든 것을 여기서 다룰 순 없다. 단지 취약점 분석 등 byte 단위 접근이 필요할 때 포맷을 이해하고 있다면 큰 도움이 되고, 그 첫 걸음이 파싱일 뿐이다. 부족함이 많은 글이지만 이것이 누군가에겐 도움이 되길 바랄뿐이다.
마지막으로 스토리지 스트림 분리는 물론 내장 VB macro 에뮬레이팅 등 OLE 파일분석에 있어 독보적인 기능을 제공하는 Decalage 를 소개하며 이 글을 마친다.
- 버전에 따라 4096 byte 단위로 저장하기도 하지만 여기선 512 byte만 다룬다. LAOLA 프로젝트에선 섹터가 아닌 블록 이란 용어를 사용한다. ↩
- FAT sectors 와 FAT array 의 관계는 LAOLA의 BBAT depot과 BBAT 의 관계와 같다. 더 정확하게는 FAT, FAT sectors, FAT array 로 구분해야 하지만 이렇게 이해해도 무리가 없다. ↩
-
파일의 크기가 클 경우, 109 개의 FAT Sector 만으론 모든 스트림의 위치를 표시하는 FAT Array 를 만들 수 없는 경우,
CompoundFileHeader.NumberOfDIFATSectors>0
이 참일경우 ↩ -
sizeof(DirectoryEntry) * 4 == SECTORSIZE == 0x200
↩ - 0xFFFFFFFF 는 값이 없다는 뜻으로 쓰이는 약속된 값이다. ↩
- 이유는 간단하다, CompoundFileHeader 에서 FAT Sector 들의 위치를 나타내는 DIFAT 멤버는 배열로 돼있지만, miniFAT Sector 의 위치를 나타내는 FirstMiniFATSectorLocation 멤버는 4byte 고정값이다. NumberOfMiniFATSectors 가 한 개를 초과할 경우, CompoundFileHeader 만으론 표현할 방법이 없다. ↩
- 큰 스트림과 작은 스트림을 나누는 기준은 CompoundFileHeader.MiniStreamCutoffSize 을 따른다. 큰 스트림은 FAT Array(BBAT), 작은 스트림은 miniFAT Array(SBAT) 를 참조한다. ↩
- miniFAT Array 를 참조하고 싶어도, 아직 miniFAT stream 이 완성되지 않아 사용할 수 없단 점을 기억하자. ↩
파일의 끝을 알기 어려운 OLE 포맷 상, extra data 의 존재유무를 확인하는 방법에대한 소개. 링크