As it has been discussed in the previous article for the executable view of an ELF file, it is necessary that the binary contains the Program Header Table.
This table contains information about the segments in the file. Specifically, each entry of the table is a struct containing information about one specific segment. It is important to remember that each ELF can be divided in segments and in sections. Each segment is composed by one or more sections.
Structure of one entry
Each struct has the following structure:
typedef struct {
uint32_t p_type;
uint32_t p_flags;
ElfN_Off p_offset;
ElfN_Addr p_vaddr;
ElfN_Addr p_paddr;
uintN_t p_filesz;
uintN_t p_memsz;
uintN_t p_align;
} ElfN_Phdr;
Again, N stands for 32 or 64. Before diving into an example, let’s discuss in detail each field.
p_type
p_type represents the type of segment referred to this entry.
- 0 for PT_NULL
- 1 for PT_LOAD. A segment of type PT_LOAD is going to be loaded in memory (e.g., TEXT and DATA)
- 2 for PT_DYNAMIC. A segment of this type is specific for a binary which is dynamically linked and in fact contains information for the dynamic linker (e.g., address of GOT, list of necessary shared libraries to be linked.
- 3 for PT_INTERP. A segment of this type contains the address of a NULL-terminated string representing the name of the interpreter to use for this binary and its length.
- 4 for PT_NOTE. A segment of this type contains the address and size of auxiliary information.
- 5 for PT_SHLIB. A segment of this type has unspecified behavior. *6 for PT_PHDR. A segment of this type contains the location of the Program Header Table itself (both in the file and in the memory image).
- In Linux usually a segment of type PT_GNU_STACK is used to control the system stack using the value in p_flags.
p_flags
p_flags contains the flags associated with the segment. The most important are R, W and E. As it is obvious, these indicate whether the segment is Readable, Writable and Executable.
p_offset
p_offset represents the offset in the file for the first byte of the segment.
p_vaddr
p_vaddr represents the offset in the memory for the first byte of the segment.
p_paddr
p_paddr represents the physical address of the segment. This is not always relevant.
p_filesz
p_filesz contains the number of bytes in the file image of the segment
p_memsz
p_memsz similarly to filesz contains the number of bytes in the memory image of the segment.
p_align
p_align describes the value to which the segment is aligned. If this value is 0 or 1, no alignment is required, otherwise it has to be a power of 2 and also p_addr needs to be p_offset mod p_align
Practical example
Let’s go now through a practical example. We will use the same binary produced in the previous article and we will look inside the segment headers.
To do this, I will use pyelftools rather than the usual readelf tool.
from elftools.elf.elffile import ELFFile
elf_file = ELFFile(open("./elfheader/code1"))
for segment in elf_file.iter_segments():
segmentHeader = segment.header
segType = segmentHeader['p_type']
segOffset = segmentHeader['p_offset']
segAlign = segmentHeader['p_align']
segSize = segmentHeader['p_filesz']
print "Starting printing information about new segment"
print "p_type: %s" % segType
print "p_flags: %s" % segmentHeader['p_flags']
print "p_offs: %s" % segOffset
print "p_vaddr: %s" % segmentHeader['p_vaddr']
print "p_paddr: %s" % segmentHeader['p_paddr']
print "p_filesz: %s" % segSize
print "p_memsz: %s" % segmentHeader['p_memsz']
print "p_align: %s" % segAlign
if segType == "PT_INTERP":
fp = open("./elfheader/code1")
fp.seek(segOffset)
interpString = fp.read(segSize).rstrip('\0')
print "Interpreter: %s" % interpString
And let’s now see the output.
p_type: PT_PHDR
p_flags: 5
p_offs: 64
p_vaddr: 4194368
p_paddr: 4194368
p_filesz: 504
p_memsz: 504
p_align: 8
Starting printing information about new segment
p_type: PT_INTERP
p_flags: 4
p_offs: 568
p_vaddr: 4194872
p_paddr: 4194872
p_filesz: 28
p_memsz: 28
p_align: 1
Interpreter: /lib64/ld-linux-x86-64.so.2
Starting printing information about new segment
p_type: PT_LOAD
p_flags: 5
p_offs: 0
p_vaddr: 4194304
p_paddr: 4194304
p_filesz: 2052
p_memsz: 2052
p_align: 2097152
Starting printing information about new segment
p_type: PT_LOAD
p_flags: 6
p_offs: 3600
p_vaddr: 6295056
p_paddr: 6295056
p_filesz: 552
p_memsz: 560
p_align: 2097152
Starting printing information about new segment
p_type: PT_DYNAMIC
p_flags: 6
p_offs: 3624
p_vaddr: 6295080
p_paddr: 6295080
p_filesz: 464
p_memsz: 464
p_align: 8
Starting printing information about new segment
p_type: PT_NOTE
p_flags: 4
p_offs: 596
p_vaddr: 4194900
p_paddr: 4194900
p_filesz: 68
p_memsz: 68
p_align: 4
Starting printing information about new segment
p_type: PT_GNU_EH_FRAME
p_flags: 4
p_offs: 1716
p_vaddr: 4196020
p_paddr: 4196020
p_filesz: 60
p_memsz: 60
p_align: 4
Starting printing information about new segment
p_type: PT_GNU_STACK
p_flags: 6
p_offs: 0
p_vaddr: 0
p_paddr: 0
p_filesz: 0
p_memsz: 0
p_align: 16
Starting printing information about new segment
p_type: PT_GNU_RELRO
p_flags: 4
p_offs: 3600
p_vaddr: 6295056
p_paddr: 6295056
p_filesz: 496
p_memsz: 496
p_align: 1
As we can see there are 2 PT_LOAD segments, one with flags 6 and one with flags 5. One of them is R+W (DATA) , while the other is R+E (TEXT).
Moreover, we checked what’s pointed by the interpreter segment as we found /lib64/ld-linux-x86-64.so.2
The last observation is that the stack’s size is 0 at the moment, but the alignment is 16, the standard in-memory alignment and the stack is marked R+W.
For any correction, feedback or question feel free to drop a mail to security[at]coolbyte[dot]eu.