Tạo file ZIP tự khởi động trên PC x86
Một lập trình viên đã tạo ra file ZIP vừa là kho lưu trữ hợp lệ, vừa chứa mã khởi động để chạy trực tiếp trên PC x86 từ đĩa mềm hoặc ổ cứng, nhờ kỹ thuật ghép định dạng (polyglot) giữa ZIP, Multiboot và boot sector BIOS.

Một chia sẻ kỹ thuật trên blog Goliath32 gây chú ý trên Hacker News mô tả cách tạo ra một file ZIP "tự khởi động": nó vừa là kho lưu trữ ZIP hoàn toàn hợp lệ, vừa chứa mã khởi động đủ để chạy trên phần cứng PC x86 thật. Điều đáng nói là file này không chỉ chạy được nhờ Qemu mà còn khởi động được từ đĩa mềm và ổ cứng.
Đây là ví dụ điển hình cho kỹ thuật polyglot — một file cùng lúc hợp lệ với nhiều định dạng khác nhau. Thành quả là file ZIP chứa cả mã nguồn cần thiết để tự build lại chính nó từ đầu.
Từ CPIO, TAR đến ZIP
Tác giả bắt đầu từ câu chuyện initramfs của Linux: kernel hỗ trợ giải nén CPIO, một định dạng đơn giản và chuẩn hóa. Khi bàn về việc có nên hỗ trợ thêm TAR hay không, lý do được nhấn mạnh là CPIO có chuẩn rõ ràng và ít bị phân mảnh.
Trong khi đó, ZIP từng là định dạng nén phổ biến nhất thời DOS/WinDOS trước khi RAR nổi lên trong thập niên 2000. Chính thiết kế của ZIP khiến nó dễ bị lợi dụng để tạo file polyglot, và ngày xưa từng tồn tại self-extracting ZIP — file EXE/ZIP vừa chạy được vừa tự giải nén, hữu ích khi máy không có phần mềm giải nén.
Vì sao ZIP dễ bị "ghép" định dạng
Một file ZIP gồm các local file header gắn liền với dữ liệu từng file, và một central directory ở cuối file, trỏ ngược lại các header. Khác với các định dạng bắt đầu bằng header, ZIP kết thúc bằng footer.
Cấu trúc định dạng ZIP
Thiết kế đọc từ cuối này rất hữu ích thời ZIP còn bị chia nhỏ qua nhiều đĩa mềm: chỉ cần quét thư mục trên đĩa cuối rồi yêu cầu người dùng lắp đĩa chứa file cần lấy. Local file header còn cho phép tới 64k dữ liệu "extra" giữa header và nội dung file, footer có trường comment cũng tới 64k.
Tuy nhiên, theo tác giả, footer có một lỗ hổng thiết kế: kích thước thay đổi, magic và kích thước lại nằm trước trường comment. Cách các trình nén xử lý tình huống này khác nhau có thể khiến một file được thiết kế khéo léo hiện ra những nội dung khác nhau tùy phần mềm. Vì vậy nhiều công cụ hiện đại chọn cách quét tuần tự từ đầu như với tarball, từ chối file có khoảng trống hoặc cấu trúc bất thường — qua đó chặn bớt thủ thuật polyglot, nhưng cũng làm một số file hợp lệ trở nên không đọc được.
Bài toán: vừa là ZIP, vừa là boot sector
Yêu cầu đặt ra: file phải bắt đầu bằng một local file header, không có khoảng trống, mọi thứ nằm gọn trong các file của kho lưu trữ. Đồng thời, 512 byte đầu tiên phải là boot sector hợp lệ để BIOS nạp vào địa chỉ 0x7c00 rồi nhảy tới.
Thủ thuật nằm ở chỗ chọn các giá trị header vừa có ý nghĩa với ZIP, vừa là opcode 8086 hợp lệ.
- Bốn byte đầu buộc phải là magic
"PK\03\04". Trong đó chữPtình cờ giải mã thành lệnhpush %ax, không gây hại nếu con trỏ stack còn hợp lý. - Trường version được đặt thành
0x000a, tương ứng phiên bản 1.0 trên MS DOS, đồng thời là một lệnhormột byte. - Trường flags là
0x8002: bit thấp nhất không mang ý nghĩa với phương thức nén không nén (method 0), còn bit cao nhất được ghi là "dành riêng cho PKWARE". Quan trọng hơn, opcode0x02là phép cộng gián tiếp một byte, và bit cao trong trường mod-reg-r/m khiến CPU đọc thêm 2 byte — đủ để "nuốt" trường compression method mà vẫn phải giữ bằng 0. - Trường timestamp là nơi đặt lệnh nhảy
0xebtương đối — giá trị này ứng với khoảng 8 giờ 30 phút sáng, tùy khoảng cách nhảy.
Nhờ đó, sau header ZIP, mã 16 bit ở chế độ thực bắt đầu tại offset 0x50 và có thể thực thi.
Mã khởi động 16 bit trong file ZIP
Multiboot và boot loader tự mang theo
File ZIP này còn chứa cả header Multiboot với magic 0x1BADB002 (đọc kiểu little endian). Multiboot là cách đơn giản để chạy chương trình trực tiếp trên x86 "bare metal", được các boot loader như GRUB hay chế độ direct kernel boot của Qemu hỗ trợ.
Mã 16 bit sau boot sector sẽ thiết lập môi trường mà mã Multiboot cần: bật cổng A20, chuyển sang chế độ bảo vệ 32 bit, rồi nhảy tới entry point ở offset 0x331. Toàn bộ mã này, gồm cả các header và bảng GDT, chỉ chiếm 817 byte.
Mã C sau đó có thể duyệt chính file ZIP để định vị và truy cập các file bên trong, nhờ thủ thuật linker script gộp mọi thứ thành một khối, với GDT và header Multiboot hiển thị ở hai vị trí khác nhau trong bộ nhớ thấp và bộ nhớ cao.
Khởi động trên máy thật
Ngoài việc chạy qua Qemu, file ZIP còn khởi động được từ đĩa mềm và ổ cứng: nó tự nhận diện ổ INT 13h mà nó được nạp từ đó và truy vấn thông số hình học của đĩa. Tác giả đã thử trên Pentium MMX, i486 DX2 và Am386DX-40; chiếc 486 có ổ đĩa mềm chậm đến mức phải "nín thở" chờ. Dòng chữ hiện trên màn hình lấy từ file boot/MESSAGE.TXT.
Để tự build, người dùng cần cấp quyền thực thi cho mk.sh, cài Zlib cho các công cụ đi kèm, rồi chạy script trong thư mục của nó để tạo bản sao mới của file ZIP khởi động được. Tác giả cho biết mã nguồn có nhiều chú thích, kèm README chỉ ra cấu trúc và điểm nên đọc trước, và cố gắng giữ mã dễ đọc thay vì nhồi tất cả vào một sector.
Góc nhìn
Với lập trình viên và kỹ sư hệ thống, dự án này là bài học thực tế về cách các định dạng dữ liệu tưởng như "đóng" lại có thể bị lợi dụng khi mọi trường header đều có thể mang ý nghĩa kép. Những ai làm việc với firmware, boot loader hay định dạng file nhị phân sẽ thấy rõ giá trị của việc hiểu đến từng byte.
Ở chiều ngược lại, nó là lời nhắc về an toàn: file ZIP đến từ nguồn không tin cậy có thể chứa nhiều hơn những gì trình giải nén hiển thị. Các công cụ xử lý file nén cần xử lý nhất quán với các trường hợp biên như comment trong footer, thay vì lựa chọn cách quét khác nhau giữa các trình nén.
Nguồn: goliath32.com (bài gốc ngày 14 tháng 11, 2026) · Quy trình biên tập


