Search code examples
windowsreverse-engineeringportable-executable

Why does section virtual addresses need to be continuous?


I am currently working on something in the work that requires me to delete the section from the executable file in PE format. At first, I have just removed IMAGE_SECTION_HEADER, changed NumberOfSections field in IMAGE_FILE_HEADER, recalculated SizeOfImage/SizeOfHeaders in IMAGE_OPTIONAL_HEADER and moved the raw address of following sections by the raw size of deleted section. However, Windows refused to load the file with the error message "XXX is not a valid Win32 application". I have been struggling for a while but tried to also move virtual address of following sections by the virtual size of deleted sections and everything works fine.

What is the reason that virtual addresses need to be continuous and there cannot be any gap? I have tried to read through official PE documentation, but unsuccessfully. I have always thought that it doesn't matter what address individual sections have as long as SizeOfImage has right value.


Solution

  • Most applications don't need to have large gaps between sections--the ones that do can use separate DLLs or allocate memory dynamically through VirtualAlloc.

    UPDATE: After more testing, I found the gap between sections must be that required to align the next section to satisfy the SectionAlignment. Hence, spacing two 4096-byte sections 0x20000 bytes apart requires setting the whole exe's SectionAlignment field to 0x20000. (A spacing of, say, 0x12000 is impossible.) This 128kB gap does not appear in the process's memory map and consumes no memory, but VirtualAlloc will still fail with ERROR_INVALID_ADDRESS if you attempt to allocate memory inside the gap.

    Here is a minimal test case for GCC/MinGW demonstrating that the sections in a Win32 exe must be contiguous.

    testcase.S:

    .global _main
    .section .text
    _main:
        push $1
        call _Sleep@4
        xor %eax, %eax
        ret
    
    .section .bss
        .lcomm buf, NUMBYTES
    

    This produces a valid executable:

    gcc -m32 -Wl,--image-base=0x00400000 -Wl,-Ttext=0x00401000 -Wl,-Tbss=0x00402000 -DNUMBYTES=0xfe000 -Wl,--section-start=.idata=0x00500000 -s -nostartfiles -o contiguous.exe testcase.S

    This produces an invalid executable (gap.exe is not a valid Win32 application):

    gcc -m32 -Wl,--image-base=0x00400000 -Wl,-Ttext=0x00401000 -Wl,-Tbss=0x00402000 -DNUMBYTES=0x200 -Wl,--section-start=.idata=0x00500000 -s -nostartfiles -o gap.exe testcase.S

    Comparing with a hex editor and objdump, there is only a 16-byte difference between the two files. What changed are:

    1. The timestamp (4-byte change).
    2. The checksum (4-byte change).
    3. The size of the .bss section (8-byte change). In contiguous.exe, the .bss section is 0xfe000 bytes large, while in gap.exe the .bss section is 0x200 bytes large, creating a gap of size 0xfc000 bytes between it and the .idata section.