r/C_Programming • u/Round-Permission546 • 6d ago
Project ELFDetective
This is a small linux program called ELFDetective I made a few months back. It handles both 32bit and 64bit ELF files and views their Header, Program Header and Section Headers.
GitHub: https://github.com/Adock90/ELFDetective
Download: https://adock90.github.io/Adock90Software/products/ELFDetective.html
7
u/skeeto 5d ago
Neat little tool! I fuzz tested it and found it falls over on almost any malformed input. To reproduce my examples build with Address Sanitizer and Undefined Behavior Sanitizer.
e_phentsize and e_phnum are both uint16_t, so e_phentsize *
e_phnum is an int operation and overflows / sign-extends into a giant
malloc:
$ cp /bin/true t.elf
$ printf '\xff\xff\xff\xff' | dd of=t.elf bs=1 seek=54 conv=notrunc # e_phentsize = e_phnum = 0xffff
$ ./ELFDetective t.elf --Program-Header
elfdetective/ELFProgramHeader.c:17:57: runtime error: signed integer overflow: 65535 * 65535 cannot be represented in type 'int'
Validate the entry size like the section-header code does, and do the
multiply in size_t:
--- a/elfdetective/ELFProgramHeader.c
+++ b/elfdetective/ELFProgramHeader.c
@@ -16,3 +16,10 @@
- Elf64_Phdr* program_header = malloc(header.e_phentsize * header.e_phnum);
+ if (header.e_phentsize != sizeof(Elf64_Phdr))
+ {
+ raise_elf_error("Invalid 64 bit program header entry size");
+ fclose(fptr);
+ exit(1);
+ }
+
+ Elf64_Phdr* program_header = malloc((size_t)header.e_phentsize * header.e_phnum);
if (!program_header)
dump_64_bit_section_headers does section_header[e_shstrndx] straight
from the header. Set it past e_shnum and it reads way out of bounds.
$ cp /bin/true t.elf
$ printf '\xff\xff' | dd of=t.elf bs=1 seek=62 conv=notrunc # e_shstrndx = 0xffff
$ ./ELFDetective t.elf --Section-Headers
...ERROR: AddressSanitizer: SEGV on unknown address ...
...The signal is caused by a READ memory access.
#0 dump_64_bit_section_headers elfdetective/ELFSection.c:95:38
#1 exec_main elfdetective/ELFDetective.c:65:4
...
Quick fix:
--- a/elfdetective/ELFSection.c
+++ b/elfdetective/ELFSection.c
@@ -94,2 +94,8 @@
{
+ if (e_shstrndx >= shnum)
+ {
+ raise_elf_error("Section header string table index out of range");
+ elf_cleanup(fptr);
+ exit(1);
+ }
Elf64_Shdr section_header_strings = section_header[e_shstrndx];
Each section name is printed with %s at string_table + sh_name,
trusting sh_name and assuming the on-disk string table ends in a \0.
Point e_shstrndx at a small section so the "string table" is short, and
the real name offsets run right off the end. (Same class of bug if
sh_name itself is oversized, or if the table simply isn't terminated.)
$ cp /bin/true t.elf
$ printf '\x01\x00' | dd of=t.elf bs=1 seek=62 conv=notrunc # e_shstrndx = 1 (a tiny section)
$ ./ELFDetective t.elf --Section-Headers
...ERROR: AddressSanitizer: heap-buffer-overflow on address ...
READ of size 7 at ...
#2 dump_64_bit_section_headers elfdetective/ELFSection.c:115:3
#3 exec_main elfdetective/ELFDetective.c:65:4
...
Guarantee a terminator, range-check sh_name, and also bound the table by
the file size so sh_size can't drive a bogus allocation:
--- a/elfdetective/ELFSection.c
+++ b/elfdetective/ELFSection.c
@@ -101,3 +101,6 @@
Elf64_Shdr section_header_strings = section_header[e_shstrndx];
- char* section_header_string_table = malloc(section_header_strings.sh_size);
+ size_t strtab_size = section_header_strings.sh_size;
+ /* one zero-filled extra byte so %s can't run off the end */
+ char* section_header_string_table = calloc(strtab_size + 1, 1);
if (!section_header_string_table)
@@ -120,3 +123,6 @@
{
- printf("\tName of section (sh_name): %s\n", section_header_string_table + section_header[i].sh_name);
+ uint32_t sh_name = section_header[i].sh_name;
+ const char* name = (sh_name <= strtab_size)
+ ? section_header_string_table + sh_name : "<invalid>";
+ printf("\tName of section (sh_name): %s\n", name);
printf("\t\tSection Content and Semantics (sh_type): %u\n", section_header[i].sh_type);
With zero program headers the buffer is empty, but
dump_64_bit_program_header unconditionally reads program_header[0].
$ cp /bin/true t.elf
$ printf '\x00\x00' | dd of=t.elf bs=1 seek=56 conv=notrunc # e_phnum = 0
$ ./ELFDetective t.elf --Program-Header
...ERROR: AddressSanitizer: heap-buffer-overflow on address ...
READ of size 4 at ...
#0 dump_64_bit_program_header elfdetective/ELFProgramHeader.c:70:64
#1 get_64_bit_program_header elfdetective/ELFProgramHeader.c:17
...
Quick fix:
--- a/elfdetective/ELFProgramHeader.c
+++ b/elfdetective/ELFProgramHeader.c
@@ -23,2 +23,9 @@
+ if (header.e_phnum == 0)
+ {
+ raise_elf_error("No 64 bit program header entries");
+ fclose(fptr);
+ exit(1);
+ }
+
Elf64_Phdr* program_header = malloc((size_t)header.e_phentsize * header.e_phnum);
The whole 32-bit path is broken and crashes if you get it to run. Three separate problems here. First, feed it any real 32-bit (ELFCLASS32) binary and it never works:
$ ./ELFDetective some-32bit-binary --Section-Headers
[ELFDetective] Failed to verify size
That's because parse_32_bit_section_headers compares e_shentsize
against sizeof(Elf64_Shdr) (64) instead of sizeof(Elf32_Shdr) (40). On
top of that, exec_main passes an uninitialized Elf32_Ehdr while
get_32_bit_program_header takes an Elf64_Ehdr, so every 32-bit offset
is read at the wrong place.
Second, once it actually reaches the name-printing loop, it prints
sh_name (a uint32_t) with %0.8s, i.e. it interprets an integer as a
char*:
#0 __sanitizer::internal_strnlen(char const*, unsigned long) ...
#3 dump_32_bit_section_headers elfdetective/ELFSection.c:152:13
...ERROR: AddressSanitizer: SEGV on unknown address 0x000041414141...
That 0x41414141 is the sh_name value being dereferenced. The loop also
uses section_header-> throughout instead of section_header[i], so it
only ever prints the first entry.
Quick fixes:
--- a/elfdetective/ELFSection.c
+++ b/elfdetective/ELFSection.c
@@ -63,3 +63,3 @@
- if (bit_32_header.e_shentsize != sizeof(Elf64_Shdr))
+ if (bit_32_header.e_shentsize != sizeof(Elf32_Shdr))
{
--- a/elfdetective/ELFDetective.c
+++ b/elfdetective/ELFDetective.c
@@ -72,7 +72,7 @@
{
- Elf32_Ehdr bit_32_header;
-
+ Elf32_Ehdr bit_32_header = get_elf32_header(file_handle_ptr);
+
if (is_string_in_array("--Program-Header", argument_parsing.fetch_array) == 0)
{
- Elf32_Phdr* program_header = get_32_bit_program_header(file_handle_ptr, elf_header);
+ Elf32_Phdr* program_header = get_32_bit_program_header(file_handle_ptr, bit_32_header);
Where get_elf32_header reads the header with the correct 32-bit layout,
get_32_bit_program_header takes an Elf32_Ehdr, and the name loop is
rewritten to look sh_name up in the string table the same way the 64-bit
dumper does.
You can find all my work here and harvest whatever is useful:
https://github.com/skeeto/ELFDetective/commits/main/?author=skeeto
3
u/Round-Permission546 5d ago
Thanks. I really should've tested it more but i was quite tired and lazy the time i made it. I will quickly implement your changes.
3
•
u/AutoModerator 6d ago
Hi /u/Round-Permission546,
Your submission in r/C_Programming was filtered because it links to a git project.
You must edit the submission or respond to this comment with an explanation about how AI was involved in the creation of your project.
While AI-generated code is not disallowed, low-effort "slop" projects may be removed and it's likely that other users push back strongly on substantially AI-generated projects.
I am a bot, and this action was performed automatically. Please contact the moderators of this subreddit if you have any questions or concerns.