Have you ever wondered what happens behind the scenes when you launch a program on your computer? Whether it’s a simple text editor on Linux or a complex game on Windows, programs aren’t just raw code; they’re meticulously organized within specific file formats. Understanding these binary formats—the Portable Executable (PE) for Windows and the Executable and Linkable Format (ELF) for Linux—is fundamental to grasping how software works, how it’s loaded into memory, and even how it can be analyzed or exploited.
At its core, a binary is a self-contained file containing executable binary code (machine instructions) and data (variables, constants, and the like) that a computer system performs computations with. These binaries are typically produced through a compilation process, which translates human-readable source code (like C or C++) into machine code that your processor can execute. This process involves assembly and linking, which combine machine instructions and data into these executable files.
Let’s explore the anatomy of PE and ELF files, highlighting their structures and how they facilitate the organization and loading of programs.
The Portable Executable (PE) Format: Windows’ Blueprint
The PE format describes the structure of modern Windows program files, including .exe, .dll, and .sys files. It’s a modified version of the Common Object File Format (COFF) and is sometimes referred to as PE/COFF. The PE format contains x86 instructions, data, and metadata necessary for a program to run.
The primary purposes of the PE format are to:
- Tell Windows how to load a program into memory, including which parts of the file to load and where in the program’s code execution should begin.
- Specify dynamically linked code libraries (DLLs) that should be loaded.
- Supply resources a running program might use, such as strings for GUI dialogs or images.
- Provide security data, like digital code signatures, to ensure code comes from a trusted source.
A typical PE file includes a series of headers that instruct the operating system on program loading, followed by sections containing the actual program data.
Key Components of a PE File:
- MS-DOS Header and MS-DOS Stub:
◦ This is a unique feature of PE files, a relic from the 1980s that provides backward compatibility with old MS-DOS systems.
◦ Every PE file starts with an MS-DOS header, beginning with the magic value “MZ”.
◦ Its main function for modern systems is to include a field called e_lfanew, which contains the file offset where the actual PE binary begins, allowing PE-aware loaders to skip to the relevant part. - PE Signature, File Header, and Optional Header:
◦ These three parts are analogous to ELF’s single executable header.
◦ The PE Signature is a string “PE” followed by two NULL characters, serving a similar purpose to ELF’s magic bytes.
◦ The PE File Header describes general file properties, such as the Machine field (indicating the CPU architecture, e.g., x86-64) and NumberOfSections. It also contains Characteristics flags that indicate if it’s a DLL or if it has been stripped.
◦ The PE Optional Header is critical for executables. It defines the program’s entry point (AddressOfEntryPoint), which is the first instruction the program runs once loaded. It also includes ImageBase, the preferred virtual address for loading the binary. Many other pointers in PE files are Relative Virtual Addresses (RVAs), offsets from this base address. - Section Headers:
◦ These describe the data “sections” within a PE file. A section is a chunk of data that will either be mapped into memory or provide instructions for the loading process.
◦ Section headers tell Windows what permissions (readable, writable, executable) apply to each section. For example, a code section (.text) is typically readable and executable but not writable to prevent self-modification.
◦ Unlike ELF, PE section names are limited to 8 characters. - Common PE Sections:
◦ .text: Contains the program’s executable x86 code.
◦ .idata (imports): Crucial for analysis, this section contains the Import Address Table (IAT), which lists dynamically linked libraries and their functions. Inspecting the IAT can reveal a malware’s high-level functionality.
◦ Data Sections (.rsrc, .data, .rdata): Store resources like mouse cursor images, button skins, audio, printable character strings, and other media used by the program. The .rsrc (resources) section is vital for malware analysts as it can provide clues about file functionality.
◦ .reloc: This section helps overcome the fact that PE binary code is not position independent. It contains information that allows the Windows operating system to translate memory addresses if the code is loaded at an unexpected location. - DataDirectory: Provides the loader with shortcuts to specific portions of the binary needed for setting up execution. It’s the closest PE has to ELF’s execution view, as the section header table in PE is used for both linking and loading.
Tools for PE Analysis: Popular tools for inspecting PE files include the Python library pefile, objdump, and strings to extract printable strings.
The Executable and Linkable Format (ELF): Linux’s Standard
ELF is the default binary format used on Linux-based systems. It’s a versatile format used for various file types, including executable files, object files, shared libraries, and core dumps. While similar to PE in its purpose, ELF has its own distinct structure.
ELF binaries essentially consist of four main components:
- An executable header
- A series of (optional) program headers
- A number of sections
- A series of (optional) section headers (one per section)
Key Components of an ELF File:
- Executable Header:
◦ Always at the beginning of an ELF file. It’s a structured series of bytes providing crucial information for the ELF specification.
◦ The header begins with a 16-byte array called e_ident, which starts with a 4-byte “magic value” (0x7f, ‘E’, ‘L’, ‘F’) that identifies the file as an ELF binary. This array also specifies details like the binary’s bit width (32-bit or 64-bit, via EI_CLASS), endianness (EI_DATA), and the ELF specification version (EI_VERSION).
◦ Other important fields include e_type (e.g., ET_REL for relocatable object file, ET_EXEC for executable, ET_DYN for dynamic library), and e_machine (indicating the target architecture, such as EM_X86_64 for 64-bit x86 systems).
◦ The e_entry field specifies the virtual address where execution should start.
◦ e_phoff and e_shoff indicate the file offsets to the program header table and section header table, respectively. - Section Headers:
◦ Each section in an ELF binary is described by a section header, which denotes the section’s properties. Sections logically divide code and data into contiguous, non-overlapping chunks.
◦ The section header table is optional if the ELF file does not require linking.
◦ The sh_flags field provides additional information, such as SHF_WRITE (section is writable at runtime), SHF_ALLOC (contents are loaded into virtual memory), and SHF_EXECINSTR (section contains executable machine instructions). - Common ELF Sections:
◦ SHT_PROGBITS sections contain program data, like machine instructions or constants.
◦ Symbol tables (SHT_SYMTAB, SHT_DYNSYM) and string tables (SHT_STRTAB) are special section types that contain symbolic information (e.g., names of functions, variables).
◦ .init and .fini: Sections that contain functions executed before and after the binary’s main entry point, respectively.
◦ .plt (Procedure Linkage Table) and .got (Global Offset Table): These sections are crucial for implementing lazy binding, where references to shared libraries are resolved only when they are first invoked at runtime.
◦ .dynamic: This section acts as a “road map” for the operating system and dynamic linker to load and set up the ELF binary for execution.
◦ .interp: Linux ELF binaries often have a special .interp section that specifies the path to the interpreter (typically /lib64/ld-linux-x86-64.so.2 on Linux) responsible for loading the binary and performing necessary relocations. - Program Headers:
◦ The program header table provides a segment view of the binary, which is used by the operating system and dynamic linker when loading an ELF into a process for execution.
◦ An ELF segment bundles zero or more sections into a single chunk, essentially providing an execution-time view.
◦ p_type identifies the segment type. PT_LOAD segments are intended to be loaded into memory. PT_INTERP segments contain the .interp section, and PT_DYNAMIC segments contain the .dynamic section.
Loading and Execution of an ELF Binary:
When you run an ELF binary, the operating system sets up a new process with a virtual address space. It then maps an interpreter (like ld-linux.so on Linux) into the process’s virtual memory. The kernel transfers control to this interpreter, which then loads the binary, performs relocations (resolving references to shared libraries), and finally transfers control to the binary’s entry point, initiating normal program execution.
It’s important to note that a binary’s in-memory representation doesn’t always directly correspond to its on-disk representation. For example, large regions of zero-initialized data might be collapsed on disk to save space but expanded in memory.
Tools for ELF Analysis: Common tools include readelf to view header details and sections, objdump for disassembling sections, file to determine file type, and ldd to explore dependencies. Libraries like libbfd and libelf provide programmatic ways to parse and manipulate ELF binaries.
Similarities and Differences: PE vs. ELF
While both PE and ELF serve the fundamental purpose of organizing code and data for execution, their designs reflect the differing philosophies and requirements of their respective operating systems (Windows vs. Linux).
Key Similarities:
- Both use headers to describe the file’s overall properties and entry points.
- Both logically divide the executable into sections (PE) or sections/segments (ELF) that contain code and data.
- Both support dynamic linking to shared libraries (DLLs in Windows, shared objects in Linux).
- Both formats contain information about the target architecture (e.g., x86, x86-64).
Key Differences: - Legacy Components: PE files include an MS-DOS header and stub for backward compatibility, which ELF files lack.
- Header Structure: ELF has a single, comprehensive executable header, whereas PE splits this information into a signature, file header, and optional header.
- Section vs. Segment Views: ELF explicitly distinguishes between sections (for linking) and segments (for loading and execution), described by separate section headers and program headers. PE, on the other hand, does not explicitly distinguish between sections and segments; its section header table serves both linking and loading purposes, with the DataDirectory providing a shortcut for the loader.
- Position Independence: PE binaries are generally not position independent and rely on a .reloc section for address translation if loaded at an alternative base address. ELF binaries often support position-independent code (PIC), which can be loaded anywhere in memory without requiring relocation.
- Entry Point Context: In PE, the AddressOfEntryPoint is a relative virtual address. In ELF, e_entry is a virtual address, and often the actual control flow is transferred to an interpreter (ld-linux.so) first, which then hands off to the e_entry point after setting up the environment.
- Symbol/Debug Info: PE usually uses separate PDB files for debugging symbols, while ELF often embeds DWARF information within the binary itself.
Understanding these binary formats is not just an academic exercise; it’s crucial for various fields, including malware analysis (to dissect and understand malicious programs without running them), reverse engineering, and binary instrumentation (inserting new code to observe or modify behavior). Whether you’re analyzing a Windows .exe or a Linux executable, knowing how these files are structured is the first step to unlocking their secrets.
