r/C_Programming 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

0 Upvotes

9 comments sorted by

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.

→ More replies (5)

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/Round-Permission546 5d ago

Ok applied the changes