[250724] 개발 일지 - bin 파일 병합
1. 이전 편 요약
하츠네 미쿠 콜라보 메인보드의 바이오스 화면을 보고 화면을 내가 원하는 이미지로 바꾸어보는 프로젝트를 계획했다.
이후 각 보드의 바이오스 파일을 다운로드받아, UI 영역을 구성하는 이미지 파일들을 추출하는 것까지는 성공하는데….
2. 이제 병합해야 하는데…
이제 콜라보 보드에서 추출한 이미지를 내가 쓰는 보드의 이미지 파일에 덮어썼다.
원본 파일만 넣기에는 용량이 너무 커서 바이오스 파일 내에 빈공 간이 없다 판단해 이미지 용량을 줄였다.
문제는 이 파일을 단순하게 .zip
파일로 압축해버린다던가 하면 당연히 안된다.1
즉, 고유한 방식으로 모든 파일들을 병합했다는 뜻인데, 이 방법은 인터넷을 찾아봐도 보이지 않았다.
하지만 인터넷에 이 파일이 어느 방식으로 구성되었는지를 분석한 자료가 있다.
https://winraid.level1techs.com/t/mod-the-text-color-theme-on-z97-asus-uefi-bios/30766/8
1.2. 헤더 구조 분석
Packer Begin (0x00000000)
- 패커의 시작 지점을 나타내는 마커
이미지 정보 섹션
- Size of image: 이미지 전체 크기
- Size of header: 헤더 크기 정보
- Image type: 이미지 유형 식별자
- Image number: 이미지 번호
- Group or sector: 그룹 또는 섹터 정보
End of header
- 헤더의 끝을 표시하는 마커
이미지 형식 정보2
- PNG 또는 JPEG: FFFFDAA0FFFF 시그니처
- BMP: 42004D0050000000FFFF 시그니처
정렬 규칙
- 4바이트 정렬: 모든 이미지가 4바이트 경계에 정렬됨
- 패딩: 크기가 4의 배수가 될 때까지 0x00으로 패딩 추가
원본 바이오스 파일에서 추출한 이미지 데이터의 hxD 데이터
3. 에라이 프로그램을 만들자
이렇게까지 복잡할 질 바에 그냥 프로그램을 만들기로 결심한다. 그래서 아래와 같이 클래스와 서로 간의 의존 관계를 구축했다.
classDiagram
direction TB
class AsusMain {
+main()
+interactive_mode()
+analyze_mode()
+extract_mode()
+repack_mode()
}
class FileUtils {
+validate_file_path()
+get_file_path_input()
+get_command_line_file()
}
class AsusFileAnalyzer {
+run_full_analysis()
+analyze_magic_bytes()
+find_patterns()
}
class AsusImageExtractor {
+run_extraction()
+detect_asus_packer_format()
+extract_images_to_directory()
}
class AsusImageRepacker {
+run_repack()
+detect_modified_images()
+repack_with_structure_preservation()
}
AsusMain --> FileUtils : "파일 검증 및 경로 처리"
AsusMain --> AsusFileAnalyzer : "파일 분석 수행"
AsusMain --> AsusImageExtractor : "이미지 추출 수행"
AsusMain --> AsusImageRepacker : "이미지 리패킹 수행"
AsusFileAnalyzer ..> FileUtils : "파일 정보 조회"
AsusImageExtractor ..> FileUtils : "디렉터리 생성"
AsusImageRepacker ..> FileUtils : "파일 경로 검증"
note for AsusMain "메인 컨트롤러<br>사용자 인터페이스 제공"
note for FileUtils "공통 유틸리티<br>파일 시스템 작업"
note for AsusFileAnalyzer "독립적 분석 모듈<br>읽기 전용 작업"
note for AsusImageExtractor "독립적 추출 모듈<br> ASUS 패키지 처리"
note for AsusImageRepacker "독립적 리패킹 모듈<br>구조 보존 재구성"
3.1. 원본 파일 구조
일단 이 파일을 다시 리패키지하기 위해서 원래 파일의 구조를 이해해야한다.
block-beta
columns 8
A["📁 파일 시작<br/>0x00000000"]:8
C["📦 ASUS Package<br/>Header (32 bytes)"]:8
D["🏷️ ASUS 시그니처<br/>00 00 00 00 20 00 00 00<br/>FF FF 00 00 FF FF 00 00<br/>00 00 00 00 00 00 00 00<br/>00 00 00 00 00 00 00 00"]:8
E["📋 이미지 #1 메타데이터<br/>(32 bytes)"]:8
F["크기(4B)<br/>Little-Endian"]:2
G["오프셋(4B)<br/>0x20 (고정)"]:2
H["특별 패턴 #1<br/>(24 bytes)<br/>FF FF 0A 00 FF FF 00 40..."]:4
I["🖼️ 이미지 #1 데이터<br/>(PNG/BMP/JPEG)"]:6
J["패딩<br/>(4바이트 정렬)"]:2
K["📋 이미지 #2 메타데이터<br/>(32 bytes)"]:8
L["크기(4B)"]:2
M["오프셋(4B)<br/>0x20"]:2
N["특별 패턴 #2<br/>(24 bytes)<br/>00 FF FF 0A 00 FF FF 02..."]:4
O["🖼️ 이미지 #2 데이터"]:6
P["패딩"]:2
Q["📋 이미지 #N 메타데이터<br/>..."]:8
R["🖼️ 이미지 #N 데이터<br/>..."]:8
S["📦 ASUS Package #2<br/>Header (32 bytes)"]:8
T["📋 Package #2 Images<br/>..."]:8
U["🔧 패키지 후<br/>데이터 영역<br/>(바이너리 데이터)"]:8
V["📁 파일 끝<br/>EOF"]:8
style A fill:#e1f5fe
style C fill:#fff3e0
style D fill:#fff3e0
style E fill:#f3e5f5
style I fill:#e8f5e8
style S fill:#fff3e0
style U fill:#e1f5fe
style V fill:#e1f5fe
위에서 말한 두 가지 사항을 이용하여 파일 압축 로직을 다음과 같이 정리하였다.
graph TD
A[프로그램 시작] --> B[원본 파일 로드]
B --> C[ASUS 패키지 구조 분석]
C --> D[수정된 이미지 감지]
D --> E{크기 변화 있음?}
E -->|No| F[바이트 단위 직접 교체]
E -->|Yes| G[구조 보존 재구성]
F --> H[원본 복사 후 교체]
G --> I[완전한 구조 재구성]
H --> J[결과 파일 저장]
I --> J
특히나 구조를 보존한 채로 재구성하는 과정은 다음과 같다.
1️⃣ 패키지 전 데이터 완전 보존
1
2
3
4
5
pkg_start = package['header_offset']
if current_pos < pkg_start:
preserved_data = self.data[current_pos:pkg_start]
new_data.extend(preserved_data)
print(f"📋 패키지 전 데이터 보존: {len(preserved_data)} bytes")
ASUS 패키지와 패키지 사이의 모든 바이너리 데이터를 바이트 단위로 완전 보존하는 것이 핵심이다.
그렇지 않다면 바이오스에서 이미지를 아예 못 띄울지도 모를테니 말이다.
2️⃣ ASUS 헤더 완전 보존
1
2
3
4
header_end = package['header_end']
original_header = self.data[pkg_start:header_end]
new_data.extend(original_header)
print(f"🏷️ ASUS 헤더 보존: {len(original_header)} bytes")
이 파일에는 가장 앞에 32바이트가량의 ASUS 시그니처 헤더가 존재한다.3 이를 원본 그대로 보존시킨다.
4️⃣ 원본 구조 완전 모방 - 핵심 알고리즘
🔢 이미지 순서 보존
1
2
# 원본 순서 유지를 위해 번호 순으로 정렬
sorted_images = sorted(package['images'], key=lambda x: x['number'])
🧱 메타데이터 구조 복원
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
for img_info in sorted_images:
# 1. 메타데이터 엔트리 (8바이트)
size_bytes = struct.pack('<I', len(img_data)) # 이미지 크기 (4바이트)
offset_bytes = struct.pack('<I', 0x20) # 고정 오프셋 (4바이트)
new_data.extend(size_bytes)
new_data.extend(offset_bytes)
# 2. 특별한 24바이트 패턴 (ASUS 고유 구조)
if img_info['number'] == 1:
# 첫 번째 이미지용 특별 패턴
special_pattern = bytes.fromhex("FFFF0A00FFFF004000000000300009040000000000000000")
else:
# 나머지 이미지용 특별 패턴
special_pattern = bytes.fromhex("00FFFF0A00FFFF0200000000300009040000000000000000")
new_data.extend(special_pattern)
🖼️ 이미지 데이터 처리
1
2
3
4
5
6
7
8
9
10
11
12
13
# 수정된 이미지인지 확인
if abs_offset in modified_images:
# 수정된 이미지 사용
img_data = modified_images[abs_offset]['new_data']
pkg_replaced_count += 1
print(f"🔄 교체: 이미지 #{img_info['number']}")
else:
# 원본 이미지 보존
img_data = img_info['data']
print(f"✅ 보존: 이미지 #{img_info['number']}")
# 이미지 데이터 추가
new_data.extend(img_data)
📏 4바이트 정렬 패딩
1
2
3
4
padding = (4 - (len(img_data) % 4)) % 4
if padding > 0:
new_data.extend(b'\x00' * padding)
print(f"🔧 패딩 추가: {padding} bytes (4바이트 정렬)")
5️⃣ 패키지 후 데이터 보존
1
2
3
4
if current_pos < len(self.data):
remaining_data = self.data[current_pos:]
new_data.extend(remaining_data)
print(f"📋 패키지 후 데이터 보존: {len(remaining_data)} bytes")