Note: This article was translated mainly by ChatGPT. If there is any mistake, please contact me.

Long ago, I knew that Windows PE executable files contained an MZ header, which is a complete DOS program. This time, for the 2023 New Year’s red packet, I finally used this long-delayed idea.

Before starting, I thought modifying the MZ header would not be too complicated. However, I ended up encountering several pitfalls and it took a long time to get it right, showing that I still have much to learn :(

Below, I’ll share the method to replace the MZ header of a PE executable file.

0x00 Prepare a DOS Executable File

Since the MZ header is a complete DOS program, naturally, you first need a DOS EXE program.

Considering part of Windows desktop applications are developed with Lazarus (the compiler is Free Pascal), I started with the DOS version of Free Pascal.

I wrote a hello, world program and tested it in DOS, and it indeed worked:

Hello Dos

However, I later found that the Free Pascal DOS Runtime was too large. A simple hello, world program had to link many Pascal Runtimes, easily exceeding 10K in size, which caused difficulties for the following steps. Therefore, I had to reconsider the approach.

Apart from Free Pascal, I reviewed modern compilers of 2023, and most no longer support building pure DOS applications. Although GCC supports compiling in 80386 real mode, Binutils does not have ready-made tools to link a valid DOS executable, so GCC’s 80386 compatibility mode seems only suitable for building programs related to system boot processes. In the end, I had to dig through last century’s software archives and returned to writing programs in assembly, then generating executables directly with MASM from the 90s.

Thankfully, I came up with pass = 2^key % 1000000007, which is extremely easy to implement in assembly. Moreover, its solution time complexity is neither too high nor too low. It’s a hash without duplicates within 10^9, and this program finally made its debut.

0x01 Analyze the Windows Executable File to be Modified

Prepare the red packet in its executable file version for x86_64, then open it with software like CFF Explorer to view its Nt Headers -> Optional Header section:

Original PE Header

There are two important points of focus here:

  1. FileAlignment: After modifying the MZ header, the sections of the modified PE file need to be aligned according to FileAlignment. Therefore, after modifying the MZ header, appropriate padding is required to meet the FileAlignment requirements. Most PE files have a FileAlignment of 0x200.
  2. BaseOfCode: This is where the size of our prepared DOS executable file is limited. Referring to the “Layout of PE File in Memory” below, since Windows loads the entire PE header into the low end of virtual memory, the new PE header length after modifying the MZ header must not exceed BaseOfCode, to ensure the PE file is self-consistent. This is the problem brought by the 10K+ DOS executable file mentioned earlier. When the MZ header is too long, we can only consider adding parts of the DOS executable file elsewhere in the file, which involves many more complex operations.

PE File in Memory Layout

0x02 Modify the DOS Executable File Header

Open a Hex editor, find the e_lfanew field at the end of the MZ header based on its definition, and calculate a valid offset value based on FileAlignment. This offset is typically n times the original PE file MZ header’s e_lfanew plus FileAlignment.

0x03 Replace the MZ Header of the Windows Executable File

Here, dd can be used to replace the MZ header quickly. I performed these operations on a Linux server. Everyone can try using environments like WSL to use dd.

Use the dd if=path_to_pe_file.exe of=path_to_pe_file_without_mz.exe bs=1 skip=$mz_header_size command to obtain a PE file with the original MZ header removed.

Use dd if=/dev/zero of=zeros.bin bs=1 count=$padding_zero_count to generate an all-zero file to fill the space between the MZ header and PE header, meeting the FileAlignment requirement.

Now you can use to generate an all-zero binary file. This is a domain name I bought for fun, haha.

Use cat new_mz.exe zeros.bin path_to_pe_file_without_mz.exe > path_to_pe_file_modified_pe.exe to implement the MZ header replacement.

0x04 Repair the Section Table of the Windows Executable File

Since the length of the MZ header changed, the offset of

the section table in the original PE file has changed. Therefore, the section table needs to be repaired to ensure the executable file can be executed normally.

Here, continue using CFF Explorer to edit the PE file:

  1. Fix the length of the PE header. Fixed PE Header
  2. Open the section table and modify each section based on its offset and size in the table, to accommodate the new length of the PE header. Fixed Section Table

After the above steps, the MZ header of a PE file is successfully replaced. Now you can open this file with both Windows and DOS to see its different behaviors on different platforms!