PT_LOAD injection and modifying the Entrypoint in C
Hello everyone, welcome back! Today I am going to write a blog about pt_load
injection. Lets get started!
What is PT_LOAD?
The PT_LOAD
is a type of program header in ELF (Executable and Linkable Format) files that defines loadable segments, which are essential for executing a program. These segments specify the portions of the file that need to be loaded into memory, including code, data, and other resources. Each PT_LOAD
entry provides details such as the segment's size in the file (p_filesz
), its size in memory (p_memsz
), the file offset where the segment begins (p_offset
), and the virtual address where it should be loaded (p_vaddr
). Permissions for the segment, such as read, write, and execute, are defined by the p_flags
field. The operating system loader uses this information to map the file's contents to the process's virtual memory. Typical programs include at least twoPT_LOAD
segments:
one for executable code (the text segment) and another for initialized data (the data segment). Additional segments may handle constants, dynamic linking, or other specialized needs. These segments are organized to ensure efficient and predictable loading of the program into memory.
To view thePT_LOAD
segments in an ELF file on a Linux system, you can use the readelf
orobjdump
commands. Below are examples:
readelf -l <path_to_elf_file>
- The
-l
option displays the program headers, includingPT_LOAD
entries. - Look for lines with
Type: PT_LOAD
in the output, which provide details about the loadable segments.
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flags Align
LOAD 0x000000 0x00400000 0x00400000 0x001000 0x001000 R E 0x1000
LOAD 0x001000 0x00401000 0x00401000 0x002000 0x004000 RW 0x1000
Here, the first LOAD
segment corresponds to the text segment (read and execute permissions), and the second to the data segment (read and write permissions).
You can also combine these commands with grep
to filter for PT_LOAD
entries:
readelf -l <path_to_elf_file> | grep LOAD
The purpose of PT_LOAD
This diagram illustrates the structure of a modified ELF file where a new segment, labeled as the “Parasite code segment,” has been injected. The ELF file starts with the Entry Point at memory address 0x7048000
, which directs execution to the beginning of the file. The ELF Header and Program Header define the structure and segments of the file, including details about the loaded segments and memory mappings.
The original segments include the Text Segment at 0x7048100
, which typically contains executable code, and the Data Segment at 0x7049000
, which holds initialized data used by the program. A new PT_LOAD segment has been added to the file to accommodate the "Parasite code segment" at 0x7079000
. This new segment is seamlessly integrated into the file, maintaining compatibility with the ELF format.
The entry point has been modified to redirect execution flow to the “Parasite code segment” first, ensuring that the injected code executes before the original program. After executing the injected code, control can return to the original program flow. This structure demonstrates how additional functionality can be injected into an ELF file without disrupting its original behavior.
(gdb) info proc mappings
process 17353
Mapped address spaces:
Start Addr End Addr Size Offset Perms objfile
0x555555554000 0x555555555000 0x1000 0x0 r--p /home/noob/ELF-binary/hello
0x555555555000 0x555555556000 0x1000 0x1000 r-xp /home/noob/ELF-binary/hello
0x555555556000 0x555555557000 0x1000 0x2000 r--p /home/noob/ELF-binary/hello
0x555555557000 0x555555558000 0x1000 0x2000 r--p /home/noob/ELF-binary/hello
0x555555558000 0x555555559000 0x1000 0x3000 rw-p /home/noob/ELF-binary/hello
0x7ffff7c00000 0x7ffff7c28000 0x28000 0x0 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7c28000 0x7ffff7db0000 0x188000 0x28000 r-xp /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7db0000 0x7ffff7dff000 0x4f000 0x1b0000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7dff000 0x7ffff7e03000 0x4000 0x1fe000 r--p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e03000 0x7ffff7e05000 0x2000 0x202000 rw-p /usr/lib/x86_64-linux-gnu/libc.so.6
0x7ffff7e05000 0x7ffff7e12000 0xd000 0x0 rw-p
0x7ffff7fa7000 0x7ffff7faa000 0x3000 0x0 rw-p
0x7ffff7fbd000 0x7ffff7fbf000 0x2000 0x0 rw-p
0x7ffff7fbf000 0x7ffff7fc3000 0x4000 0x0 r--p [vvar]
0x7ffff7fc3000 0x7ffff7fc5000 0x2000 0x0 r-xp [vdso]
0x7ffff7fc5000 0x7ffff7fc6000 0x1000 0x0 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7fc6000 0x7ffff7ff1000 0x2b000 0x1000 r-xp /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ff1000 0x7ffff7ffb000 0xa000 0x2c000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffb000 0x7ffff7ffd000 0x2000 0x36000 r--p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffff7ffd000 0x7ffff7fff000 0x2000 0x38000 rw-p /usr/lib/x86_64-linux-gnu/ld-linux-x86-64.so.2
0x7ffffffde000 0x7ffffffff000 0x21000 0x0 rw-p [stack]
0xffffffffff600000 0xffffffffff601000 0x1000 0x0 --xp [vsyscall]
Using the gdb
tool and the info proc mappings
command, you can examine how a live process's memory is allocated. This command provides a detailed list of the virtual address space assigned to different parts of the program, including their dependencies. Each row represents a memory region and includes columns for the start and end addresses, size, offset, permissions, and the associated file or memory section.
The first set of memory mappings typically corresponds to the executable file (e.g., hello
). These mappings are categorized into sections such as r--p
(read-only), r-xp
(read and execute), and rw-p
(read and write). These sections align with the .text
(code), .rodata
(read-only data), and .data
(writable data) segments of the ELF file, as defined by the PT_LOAD
entries. The r-xp
entry points to the executable code segment, while rw-p
entries represent writable segments, such as .data
or .bss
.
Following this, shared libraries like libc.so.6
and ld-linux-x86-64.so.2
are dynamically loaded into memory. Their mappings show multiple regions, such asr-xp
for executable code,r--p
for read-only data, andrw-p
for writable data. These permissions match the PT_LOAD
segment definitions in the ELF files of the shared libraries.
Additionally, special memory regions are mapped, such as [vdso]
(virtual dynamic shared object), [stack]
(program stack), and [vsyscall]
(legacy syscall interface). The[vdso]
region aids in optimizing system calls, while the[stack]
region is used for function calls and local variables.
This output makes it easy to see how memory is split up into well-defined areas with tight rights to keep things working and safe. For instance, readable code parts (r-xp) can’t be written to, which helps stop code injection attacks. It is also easy to see how dynamic linking works because shared libraries are loaded and linked while the program is running. This shows how PT_LOAD parts make library loading quick and safe while still protecting memory well.
Modifying the Entry Point
The e_entry
field in the ELF header tells you the entry point of an ELF file. This is the memory address where the program starts running. PT_LOAD injection is a common way to add and run malicious code inside a current program. One important part of this method is changing this entry point. Attackers can change the entry point to point to the address of the inserted code section. This changes the program's execution flow so that the attackers' payload is run first when the program starts up. This makes it possible for the added code to run before the original program's planned functions. As a result, attackers can use this mechanism to do bad things like setting up persistence, stealing private data, or messing with the system before giving control back to the legal program. This change is a key part of taking over the program's beginning execution flow, which lets the attacker run their code invisibly before the program's normal processes start up again.
Coding part
The diagram shows the steps that need to be taken to add a new PT_LOAD
section to an ELF (Executable and Linkable Format) file. The first step is to find the last current PT_LOAD
section. This is very important because the new segment will be placed right after it. Once the last segment is found, the next step is to figure out where the new segment should go, making sure that the memory and file offsets are lined up correctly according to the ELF format. Once the place is known, a new PT_LOAD
header is made that tells the computer what the new segment’s features are, like how big it is, where it is, and who can access it. This header is carefully put together to follow the ELF standard. After that, the ELF header is changed to include the new section. This includes changing the number of program header tables and making sure that all offsets and sizes stay the same with the new layout. Finally, the changed content is put back to the ELF file. This includes the new section and the updated headers.
1. Finding Last PT_LOAD Segment:
Elf64_Phdr *last_load = NULL;
for (int i = 0; i < ehdr->e_phnum; i++) {
if (phdr[i].p_type == PT_LOAD) {
last_load = &phdr[i];
}
}
This code snippet is written in C and is designed to find the last PT_LOAD segment in an ELF (Executable and Linkable Format) file. The program iterates through the program header table of the ELF file, which is represented by the phdr
array. The program header contains metadata about different segments of the executable.
The last_load
pointer is initialized to NULL
and will store the address of the last program header entry of type PT_LOAD
. The for
loop iterates through each program header entry, from index 0 to e_phnum
, which represents the total number of program header entries in the ELF file. Inside the loop, the if
condition checks whether the type of the current program header (phdr[i].p_type
) is equal to PT_LOAD
, which indicates a loadable segment. If the condition is true, the last_load
pointer is updated to point to the current entry (&phdr[i]
).
By the end of the loop, last_load
will point to the last program header of type PT_LOAD
in the ELF file, or it will remain NULL
if no such segment exists. This is useful for operations that require modifying or appending segments, such as injecting payloads or adding new functionality to an ELF file.
2. Calculating New Segment Position:
uint64_t page_size = 0x1000;
uint64_t new_vaddr = (last_load->p_vaddr + last_load->p_memsz + page_size - 1) & ~(page_size - 1);
uint64_t file_offset = (st.st_size + page_size - 1) & ~(pageffset = (st.st_size + page_size - 1) & ~(page_size - 1);
This code calculates aligned memory and file offsets for adding a new segment to an ELF file, ensuring compliance with page alignment requirements. First, the page_size
is set to 0x1000 (4096 bytes), which is the standard memory page size on many systems. This value is critical for aligning memory and file offsets to page boundaries. The new_vaddr
calculation determines the virtual address for the new segment by taking the virtual address of the last PT_LOAD segment (p_vaddr
), adding its size in memory (p_memsz
), and rounding up to the next page boundary using (page_size - 1)
. The bitwise AND with ~(page_size - 1)
ensures the result is aligned to a page boundary by clearing the lower bits. Similarly, the file_offset
is calculated by taking the current file size (st.st_size
), adding (page_size - 1)
to round up, and aligning it to the nearest page boundary with the same bitwise operation. These calculations are essential for maintaining the consistency between memory and file mappings and ensuring the new segment integrates seamlessly while adhering to ELF file and memory alignment rules.
3. Creating New PT_LOAD Header:
Elf64_Phdr new_phdr;
memset(&new_phdr, 0, sizeof(new_phdr));
new_phdr.p_type = PT_LOAD;
new_phdr.p_flags = PF_X | PF_R;
new_phdr.p_offset = file_offset;
new_phdr.p_vaddr = new_vaddr;
new_phdr.p_filesz = payload_size;
new_phdr.p_memsz = payload_size;
new_phdr.p_align = page_size;
This code snippet demonstrates the initialization and configuration of a new program header (Elf64_Phdr
) for adding a PT_LOAD segment to an ELF file. The process begins with the declaration of a new program header structure, new_phdr
. The memset
function is used to zero-initialize the structure to ensure that all fields are set to a known default value. The type of the new segment is then set to PT_LOAD
, indicating that this segment is loadable into memory. The p_flags
field is configured with PF_X | PF_R
, which grants the segment execute (PF_X
) and read (PF_R
) permissions.
Next, the p_offset
is set to file_offset
, specifying the location in the file where the segment's data begins. The p_vaddr
and p_paddr
fields are both assigned the new_vaddr
value, indicating the virtual and physical memory addresses where the segment will be loaded. The p_filesz
field is set to the size of the payload in the file, and the p_memsz
field is assigned the same value to indicate that the memory and file sizes of the segment are identical. Finally, the p_align
field is set to page_size
, ensuring that the segment adheres to the required memory alignment rules.
This configuration creates a new loadable segment that is aligned properly and has the necessary permissions and addresses, enabling its integration into the ELF file for execution.
4. Updating ELF Header:
uint64_t original_entry = ehdr->e_entry;
ehdr->e_shoff += page_size;
ehdr->e_entry = new_vaddr;
memcpy(&phdr[ehdr->e_phnum], &new_phdr, sizeof(Elf64_Phdr));
ehdr->e_phnum++;
This code snippet modifies the ELF file’s header to integrate a new PT_LOAD segment and updates relevant fields to ensure proper execution and structure. It begins by storing the original entry point of the ELF file (e_entry
) into the variable original_entry
, preserving it for potential future use or reference. Next, the section header offset (e_shoff
) is incremented by the page size to accommodate alignment changes introduced by adding the new segment. The ELF entry point (e_entry
) is then updated to new_vaddr
, the virtual address of the newly added segment, ensuring that execution starts from the injected payload when the program runs. The new program header (new_phdr
), which defines the properties of the added segment, is appended to the program header table at the index specified by the current program header count (e_phnum
) using the memcpy
function. Finally, the program header count (e_phnum
) is incremented to reflect the addition of the new segment. These modifications ensure the ELF file remains valid and functional while integrating the new segment into its structure and updating its execution flow.
5. The result:
noob@noob:~/rootkit/elf-injection$ ./injection test_program infected_program
[*] Original file size: 15968 bytes
[*] Payload size: 18 bytes
[*] Original entry point: 0x1060
[*] Last PT_LOAD segment ends at: 0x4018
[*] New segment will be at: 0x5000
[*] Original entry point: 0x1060
[*] New entry point: 0x5000
[*] Payload offset: 0x4000
[*] File size before padding: 15968 bytes
[*] Padding size: 416 bytes
[*] Payload size: 18 bytes
[*] Total size: 16402 bytes
[+] File successfully infected and written to infected_program
infected!
This output demonstrates the process and results of injecting a payload into an ELF file named test_program
This output demonstrates the process and results of injecting a payload into an ELF file named test_program
to create a modified version called infected_program
. The original file was 15,968 bytes in size, and a payload of 18 bytes was prepared for injection. The initial entry point of the program was at memory address 0x1060
, and the last PT_LOAD segment in the original file ended at memory address 0x4018
. To accommodate the new segment, it was placed at memory address 0x5000
, and its corresponding payload offset was set to 0x4000
.
Padding was added to ensure proper alignment for the new segment, with 416 bytes of padding applied after the original file content. The program’s entry point was updated from 0x1060
to the new segment's starting address 0x5000
, ensuring the payload is executed first. The final infected file size became 16,402 bytes, which includes the original file size (15,968 bytes), the padding (416 bytes), and the payload (18 bytes). The output confirms the success of the injection process and the creation of the modified file infected_program
, ready for execution with the injected payload.
Source code: https://github.com/0xCD4/Low-Level-Projects/tree/main/elf-injection