31 복합 파일(Compound File Binary, CFB)
.msi 파일은 복합 파일("구조적 저장소")이다: 파일 하나 안에 이름 붙은 스트림들이 든 작은 파일 시스템이다. 형식은 [MS-CFB]가 정한다. 이 문서는 MSI 작성기에 필요한 부분과 Windows 자신의 작성기가 하는 일을 다룬다.
31.1 섹터#
- 파일은 크기가 고정된 섹터의 연속이다. 3판은 512바이트 섹터(섹터 시프트 9), 4판은 4096바이트 섹터(시프트 12)를 쓴다. [spec]
- 머리(header)는 오프셋 0 에 있다. 섹터 번호
N은 두 판 모두 파일 오프셋(N + 1) * sector_size에서 시작한다. 그래서 4판에서는 머리가 4096바이트 섹터 하나를 통째로 차지하고, 머리 뒤의 바이트는 0 이다. [spec] msi.dll은 크기와 상관없이(20 KB 에서 10 MB 까지 해 보았다) 새 데이터베이스를 모두 4판으로, 부판(minor version)0x003E로 쓴다. 설치할 때는 두 판 다 받는다. rubrapack 은 기본으로 4판을 쓰고 3판도 쓸 수 있다. [observed]
체인과 표 속의 특별한 섹터 번호: [spec]
| 값 | 뜻 |
|---|---|
FFFFFFFA | 가장 큰 보통 섹터 번호 |
FFFFFFFC | DIFAT 섹터(FAT 안에서) |
FFFFFFFD | FAT 섹터(FAT 안에서) |
FFFFFFFE | 체인의 끝 |
FFFFFFFF | 빈 섹터 / 스트림 없음 |
31.2 머리(처음 512바이트) [spec]#
| 오프셋 | 크기 | 필드 | 쓰는 값 |
|---|---|---|---|
| 0 | 8 | 서명 | D0 CF 11 E0 A1 B1 1A E1 |
| 8 | 16 | CLSID | 0 |
| 24 | 2 | 부판 | 003E |
| 26 | 2 | 주판 | 3 또는 4 |
| 28 | 2 | 바이트 순서 | FFFE(바이트 FE FF) |
| 30 | 2 | 섹터 시프트 | 9 또는 12 |
| 32 | 2 | 미니 섹터 시프트 | 6(64바이트 미니 섹터) |
| 34 | 6 | 예약 | 0 |
| 40 | 4 | 디렉터리 섹터 수 | 3판은 0, 4판은 그 수 |
| 44 | 4 | FAT 섹터 수 | |
| 48 | 4 | 첫 디렉터리 섹터 | |
| 52 | 4 | 트랜잭션 서명 | 0 |
| 56 | 4 | 미니 스트림 경계 | 4096 |
| 60 | 4 | 첫 미니 FAT 섹터 | 없으면 FFFFFFFE |
| 64 | 4 | 미니 FAT 섹터 수 | |
| 68 | 4 | 첫 DIFAT 섹터 | 없으면 FFFFFFFE |
| 72 | 4 | DIFAT 섹터 수 | |
| 76 | 436 | 처음 109개 FAT 섹터 번호(DIFAT) | 안 쓰는 칸은 FFFFFFFF |
31.3 FAT, DIFAT, 미니 FAT [spec]#
- FAT 는 섹터마다 u32 하나인 배열로, 그 섹터가 속한 체인의 다음 섹터를 준다. FAT 섹터 자신은
FFFFFFFD로 표시한다. - FAT 자신의 섹터 번호는 DIFAT 에 적는다: 처음 109개는 머리에, 나머지는 DIFAT 섹터에. DIFAT 섹터 하나는
sector_size / 4 - 1개의 번호를 담고, 마지막 u32 에 다음 DIFAT 섹터(끝이면FFFFFFFE)를 담는다. DIFAT 섹터는 FAT 에서FFFFFFFC로 표시한다. - 4096바이트 섹터에서는 FAT 섹터 109개가 이미 약 457 MB 를 덮으므로, 실제 4판 MSI 파일에는 DIFAT 섹터가 없다. 512바이트 섹터에서는 약 7 MB 부터 DIFAT 가 생긴다.
- 경계(4096바이트) 보다 작은 스트림은 미니 스트림에 산다. 64바이트 미니 섹터로 나뉘고 미니 FAT(FAT 와 같은 배치)로 이어진다. 미니 스트림 자체는 루트 디렉터리 항목의 보통 스트림으로 저장된다.
FAT 는 자기 섹터와 DIFAT 의 섹터까지 덮어야 한다. 작성기는 수가 더 바뀌지 않을 때까지 되풀이해 셈한다.
31.4 디렉터리 [spec]#
디렉터리는 128바이트 항목을 담은 섹터 체인이다. 항목 0 이 루트다.
| 오프셋 | 크기 | 필드 |
|---|---|---|
| 0 | 64 | 이름, UTF-16LE, NUL 로 끝남(최대 31자 + NUL) |
| 64 | 2 | NUL 을 포함한 이름 길이(바이트) |
| 66 | 1 | 종류: 0 안 씀, 1 저장소, 2 스트림, 5 루트 |
| 67 | 1 | 색: 0 빨강, 1 검정 |
| 68 | 4 | 왼쪽 형제(FFFFFFFF = 없음) |
| 72 | 4 | 오른쪽 형제 |
| 76 | 4 | 자식(저장소와 루트만) |
| 80 | 16 | CLSID |
| 96 | 4 | 상태 비트 |
| 100 | 8 | 만든 시각 |
| 108 | 8 | 고친 시각 |
| 116 | 4 | 시작 섹터(크기에 따라 보통 또는 미니) |
| 120 | 8 | 스트림 크기(3판에서는 아래 32비트만 센다) |
- 루트 항목의 이름은
Root Entry이고, 그 시작 섹터와 크기는 미니 스트림을 가리킨다. - MSI 의 루트 항목은 CLSID
{000C1084-0000-0000-C000-000000000046}을 가지며, 바이트로는84 10 0C 00 00 00 00 00 C0 00 00 00 00 00 00 46이다. [spec] [observed] - 안 쓰는 항목은 링크 필드 셋이
FFFFFFFF인 것을 빼고 모두 0 이다. [spec] - 저장소의 자식들은 이름 길이, 그다음 대문자로 바꾼 UTF-16 이름의 비교 순으로 늘어선 레드-블랙 트리를 이룬다. [MS-CFB]는 가장 단순한 올바른 트리도 허용한다: 모든 노드가 검정이고 트리는 그냥 균형 잡힌 이진 탐색 트리. rubrapack 은 그것 - 정렬한 이름 위의 균형 트리, 모든 노드 검정 - 을 만들고,
msi.dll은 그것을 연다. [spec] [observed] - 시각은 0 이어도 된다. 시각을 0 으로 쓰는 것(그리고 시계에서 오는 다른 입력을 쓰지 않는 것)이 CFB 작성기를 결정적으로 만든다. [spec]
31.5 단순하고 결정적인 작성기#
rubrapack 은 파일을 다음 순서로, 모든 구간을 이어서 놓는다:
머리 | FAT 섹터 | DIFAT 섹터 | 디렉터리 | 미니 FAT | 미니 스트림 | 큰 스트림들을 차례로
- 스트림 크기를 "미니"(< 4096)와 "보통"으로 나눈다.
- 미니 스트림들은 64바이트 단위로 이어 붙인다. 미니 스트림 길이는 그 합이다.
- 디렉터리, 미니 FAT, 미니 스트림 섹터 수를 세고, 그다음 FAT 와 DIFAT 섹터 수를 센다(되풀이).
- 이어진 구간마다 FAT 체인을 채우고, 디렉터리 트리, 미니 FAT, FAT, DIFAT, 머리를 쓴다.
31.6 안전하게 읽기#
판독기는 다음을 따라가지 말고 거부해야 한다: 도는 체인(파일의 섹터 수 안에 FFFFFFFE 에 닿지 않는 체인), 파일 밖의 섹터 번호, 스트림 크기가 요구하는 것보다 짧은 체인, 범위 밖의 디렉터리 링크, 한 노드를 두 번 지나는 형제 트리, 상식적인 한도를 넘는 수. 어느 것도 비용이 크지 않고, 그렇지 않으면 조작된 .msi 가 판독기를 멈추게 하거나 죽인다.
31.7 실제 예: 튜토리얼의 hello.msi#
튜토리얼의 첫 패키지(2장)를 --reproducible 로 빌드하면 139264 바이트다: 머리 섹터와 4096 바이트 섹터 33개. 머리는:
| 오프셋 | 바이트 | 필드 | 값 |
|---|---|---|---|
0x00 | d0 cf 11 e0 a1 b1 1a e1 | 서명 | Compound File |
0x18 | 3e 00 | 부 판 | 0x003E |
0x1A | 04 00 | 주 판 | 4 |
0x1C | fe ff | 바이트 순서 | 0xFFFE |
0x1E | 0c 00 | 섹터 시프트 | 12: 2^12 = 4096 |
0x20 | 06 00 | 미니 섹터 시프트 | 6: 64 |
0x28 | 01 00 00 00 | 디렉터리 섹터 수 | 1 |
0x2C | 01 00 00 00 | FAT 섹터 수 | 1 |
0x30 | 01 00 00 00 | 첫 디렉터리 섹터 | 1 |
0x38 | 00 10 00 00 | 미니 스트림 경계 | 4096 |
0x3C | 02 00 00 00 | 첫 미니 FAT 섹터 | 2 |
0x40 | 01 00 00 00 | 미니 FAT 섹터 수 | 1 |
0x44 | fe ff ff ff | 첫 DIFAT 섹터 | FFFFFFFE |
0x4C | 00 00 00 00 | DIFAT[0]: 첫 FAT 섹터 | 0 |
섹터들과 각 섹터의 FAT 값(사슬의 다음 섹터):
| 섹터 | 파일 오프셋 | FAT 값 | 담은 것 |
|---|---|---|---|
| 0 | 0x1000 | FFFFFFFD | FAT |
| 1 | 0x2000 | FFFFFFFE | 디렉터리 |
| 2 | 0x3000 | FFFFFFFE | 미니 FAT |
| 3 | 0x4000 | 4 | 미니 스트림 |
| 4 | 0x5000 | 5 | 미니 스트림 |
| 5 | 0x6000 | FFFFFFFE | 미니 스트림 |
| 6 | 0x7000 | 7 | Binary.RpCa |
| 7 | 0x8000 | 8 | Binary.RpCa |
| 8 | 0x9000 | 9 | Binary.RpCa |
| 9 | 0xA000 | 10 | Binary.RpCa |
| 10 | 0xB000 | 11 | Binary.RpCa |
| 11 | 0xC000 | 12 | Binary.RpCa |
| 12 | 0xD000 | 13 | Binary.RpCa |
| 13 | 0xE000 | 14 | Binary.RpCa |
| 14 | 0xF000 | 15 | Binary.RpCa |
| 15 | 0x10000 | 16 | Binary.RpCa |
| 16 | 0x11000 | 17 | Binary.RpCa |
| 17 | 0x12000 | 18 | Binary.RpCa |
| 18 | 0x13000 | 19 | Binary.RpCa |
| 19 | 0x14000 | 20 | Binary.RpCa |
| 20 | 0x15000 | 21 | Binary.RpCa |
| 21 | 0x16000 | 22 | Binary.RpCa |
| 22 | 0x17000 | 23 | Binary.RpCa |
| 23 | 0x18000 | 24 | Binary.RpCa |
| 24 | 0x19000 | 25 | Binary.RpCa |
| 25 | 0x1A000 | 26 | Binary.RpCa |
| 26 | 0x1B000 | 27 | Binary.RpCa |
| 27 | 0x1C000 | 28 | Binary.RpCa |
| 28 | 0x1D000 | 29 | Binary.RpCa |
| 29 | 0x1E000 | 30 | Binary.RpCa |
| 30 | 0x1F000 | FFFFFFFE | Binary.RpCa |
| 31 | 0x20000 | 32 | cab1.cab |
| 32 | 0x21000 | FFFFFFFE | cab1.cab |
뿌리 디렉터리 항목, 섹터 1 의 처음 128 바이트:
| 오프셋 | 바이트 | 필드 | 값 |
|---|---|---|---|
| 0 | 52 00 6f 00 6f 00 74 00 20 00 45 00 6e 00 74 00 72 00 79 00 ... | 이름 | Root Entry (UTF-16LE) |
| 64 | 16 00 | 이름 길이 | 22 |
| 66 | 05 | 종류 | 5 (root) |
| 67 | 01 | 색 | 1 (black) |
| 76 | 07 00 00 00 | 자식 | 항목 7 |
| 80 | 84 10 0c 00 00 00 00 00 c0 00 00 00 00 00 00 46 | CLSID | {000C1084-0000-0000-C000-000000000046} |
| 116 | 03 00 00 00 | 시작 | 섹터 3 |
| 120 | 00 22 00 00 00 00 00 00 | 크기 | 8704 |
뿌리의 크기가 곧 미니 스트림의 크기다: 8704 바이트에 스트림 23개 중 21개가 64 바이트 미니 섹터로 들어 있고, 보통 섹터를 쓸 만큼 큰 스트림은 2개다. 스트림의 이름과 내용은 복합 파일 속의 MSI 데이터베이스가 이어서 보인다.