Safe Penetration Testing Labs · intermediate · ~25 min
## What you will learn - Recognise an ELF file from its 4 magic bytes (`0x7F 'E' 'L' 'F'`) and explain why every Linux binary starts with them. - Read the `e_ident` block to determine word size (32- vs 64-bit) and byte order (little- vs big-endian) *before* parsing anything else. - Decode the key fields of an `Elf64_Ehdr`: `e_type`, `e_machine`, `e_entry`, `e_phoff`, and `e_shoff`. - Build the values of multi-byte fields from raw bytes by hand, respecting the binary's declared endianness — applying what you learned in *Bitwise operations* and *Endianness and byte order*. - Write a safe, **read-only** parser that validates the magic, refuses short reads, and never trusts an in-file offset without bounds-checking it. - Cross-check your parser against `readelf -h` and `file(1)` so you know it is correct.
ELF stands for Executable and Linkable Format. It is the on-disk shape of nearly everything executable on Linux: programs (/bin/ls), shared libraries (libc.so), object files (.o), and even crash dumps (core files). When the kernel runs a program, the very first thing it reads is the ELF header. When a forensic analyst finds a mysterious file dropped on a server, the first thing they read is the ELF header too. It is the binary's calling card — a fixed, well-documented block of bytes that announces what the file is and how the rest of it is organised.
The ELF header sits at the very start of the file. On a 64-bit system it is exactly 64 bytes long (Elf64_Ehdr); on a 32-bit system it is 52 bytes (Elf32_Ehdr). Those bytes tell you three things you cannot do without:
e_phoff) and section-header table (e_shoff) live, which is where the rest of the file's structure is described.This lesson connects directly to your prerequisites. From Endianness and byte order you already know that a multi-byte integer can be stored low-byte-first (little-endian) or high-byte-first (big-endian); the ELF header literally has a byte that tells you which one this file uses, and you must honour it or every number you read will be garbage. From Bitwise operations you know how to combine individual bytes into a wider integer with shifts and OR — exactly the technique we use to read e_machine, e_entry, and the offsets by hand.
One discipline runs through the whole lesson: we read the header, we never modify it. Parsing untrusted binaries is a defensive, read-only activity. Treat every byte that came from the file as hostile until you have validated it.
The ELF header is the universal binary fingerprint, and reading it correctly is the foundation of two real jobs:
Defensive forensics and incident response. When you discover an unknown file on a host — a suspected dropper, a payload staged in /tmp, an artifact pulled from a memory image — the first question is always "what is this?" The header answers it: Is it even ELF? 32- or 64-bit? Which CPU? Is it a PIE executable, a shared object, or a core dump? You can answer all of this from the first 64 bytes without ever executing the file, which is critical: you must never run a suspicious sample on a production host.
Triage and tooling. Tools like file(1), readelf, objdump, and antivirus engines all begin by parsing this header. Package managers verify the architecture of an executable before installing it. If your parser disagrees with readelf, you have learned something about your bug or about a deliberately malformed file — malware sometimes corrupts header fields to confuse naive parsers.
Getting this wrong has security consequences. A parser that blindly trusts e_phoff or e_shoff and seeks to that offset can be steered out of bounds by a crafted file, turning a 'safe' analysis tool into a crash or a memory-disclosure bug. Robust header parsing is itself a security skill.
We will teach each field as its own concept. Throughout, remember the golden rule: validate before you trust.
Definition. The first four bytes of every ELF file are fixed: 0x7F, 'E' (0x45), 'L' (0x4C), 'F' (0x46).
Why it works this way. A leading non-printable byte (0x7F) followed by the ASCII letters ELF is a magic number: a signature unlikely to appear by accident, easy to test for, and human-readable in a hex dump. Many formats use this trick — MZ for Windows PE, \x89PNG for PNG, %PDF for PDF.
How to use it. Read at least 4 bytes, compare with memcmp, and reject anything that does not match. This is forensic triage step one.
Pitfall. Comparing as a C string. The first byte is 0x7F, not a printable character, and the buffer is binary — never use strcmp/strncmp on it, and never assume a NUL terminator. Use memcmp with an explicit length.
Knowledge check (concept): Why is
memcmp(buf, "\x7f" "ELF", 4)correct here butstrncmp(buf, "\x7fELF", 4)risky? (Hint: think about whatstrncmpdoes with embedded bytes vs. how the literal is encoded.)
e_ident — the identification block (bytes 0–15)Definition. The first 16 bytes form e_ident. After the 4 magic bytes it records, by index:
e_ident[4] = EI_CLASS: 1 = ELFCLASS32, 2 = ELFCLASS64.e_ident[5] = EI_DATA: 1 = ELFDATA2LSB (little-endian), 2 = ELFDATA2MSB (big-endian).e_ident[6] = EI_VERSION: file version, 1 = current.e_ident[7] = EI_OSABI: target OS ABI (0 = System V, 3 = Linux, etc.).e_ident[8] = EI_ABIVERSION, then padding to byte 15.Why it matters most. You must read e_ident first, because EI_CLASS tells you whether the rest of the header is 52 or 64 bytes, and EI_DATA tells you how to interpret every multi-byte field. Get these two single bytes wrong and everything after them is meaningless. They are single bytes, so they have no endianness of their own — that is deliberate, so a parser can read them before it knows the byte order.
Offset: 0 1 2 3 4 5 6 7 8 9 .. 15
Byte: 7F 45 4C 46 CLASS DATA VERSION OSABI ABIVERSION padding
\------magic------/ \---------- identity ----------/ \-- reserved --/
is it ELF? 32/64? LE/BE? ...
When NOT to skip it. Never hardcode "64-bit little-endian" because that is what your laptop is. A captured sample may target ARM big-endian; honouring EI_CLASS/EI_DATA is what makes your tool a forensic tool rather than a toy.
Pitfall. Casting the buffer straight to Elf64_Ehdr before checking EI_CLASS. On a 32-bit binary the field offsets differ, so you read the wrong bytes.
Knowledge check (predict the output): A file begins with
7F 45 4C 46 02 01 01 00 .... What is its class, and is it little- or big-endian?
e_type — what kind of object is this?Definition. A 16-bit field naming the file's role:
| Value | Name | Meaning |
|---|---|---|
| 1 | ET_REL |
relocatable object file (.o) |
| 2 | ET_EXEC |
classic fixed-address executable |
| 3 | ET_DYN |
shared library or PIE executable |
| 4 | ET_CORE |
core dump |
Forensic note. Modern Linux executables are almost always ET_DYN (PIE — Position-Independent Executable), the same value as a shared library. Distinguishing a PIE program from a .so needs more than e_type (e.g. the presence of an INTERP program header), but e_type is your first clue. Whether a binary is PIE affects how ASLR randomises it, which matters in exploit and mitigation analysis.
e_machine — which CPU?Definition. A 16-bit field naming the target architecture. Common values:
0x3E (62) = EM_X86_640xB7 (183) = EM_AARCH64 (64-bit ARM)0x28 (40) = EM_ARM (32-bit ARM)0x03 (3) = EM_386 (32-bit x86)Use. If e_machine does not match the host CPU, the binary cannot run natively here — a useful triage signal. Combined with EI_DATA, a wrong-endianness or wrong-machine file is immediately suspicious or simply foreign.
e_entry, e_phoff, e_shoff — addresses and offsetsDefinition. On ELF64 these are 64-bit values. e_entry is the virtual address of the first instruction. e_phoff is the file offset of the program-header table (used at load time). e_shoff is the file offset of the section-header table (used by linkers and tools).
Critical distinction. e_entry is a virtual address (where the code lives in the process's memory) — do not seek to it in the file. e_phoff/e_shoff are file offsets (byte positions in the file) — you may seek to them, but only after bounds-checking against the file size.
FILE on disk PROCESS memory at runtime
+------------------+ 0 +---------------------+
| ELF header (64B) | | ... |
+------------------+ | code @ e_entry <---+-- execution starts
| program headers | <-- e_phoff | ... |
+------------------+ +---------------------+
| .text, .data ... |
+------------------+
| section headers | <-- e_shoff
+------------------+
Pitfall (security-relevant). Trusting e_shoff blindly: lseek(fd, e_shoff, SEEK_SET) followed by a read can be driven past end-of-file by a malicious header, causing a short read your code mishandles, or — if you used it to index into a buffer — an out-of-bounds access. Always verify offset + size <= file_size.
Knowledge check (find the bug):
lseek(fd, ehdr.e_shoff, SEEK_SET); read(fd, sh, sizeof(sh));— what is missing, and how could a crafted file abuse it?
Parsing ELF headers is a read-only, defensive activity and is safe to practise on your own files. The moment a binary is suspicious, treat it as live malware: analyse it only inside an isolated, authorised lab (a local VM or container you own), never on a production host, and never execute it. Do not download or handle real-world malware samples without explicit authorisation and proper containment.
Threat model (analysing an unknown ELF)
Asset: analyst workstation, its data, the wider network
Entry point: bytes of the untrusted file
Trust boundary: ---- everything inside the file is UNTRUSTED ----
Defences: read-only parsing, bounds checks, no execution,
isolated VM/container, no network egress from the lab
The system header <elf.h> provides ready-made ELF types and constants, so you rarely declare the structures yourself.
#include <elf.h> /* Elf64_Ehdr, EI_CLASS, ELFCLASS64, EM_X86_64, ... */
#include <stdint.h> /* uint16_t, uint64_t */
/* Selected fields of the 64-bit header (declared for you by <elf.h>): */
/* typedef struct { */
/* unsigned char e_ident[16]; // magic + class/data/version/abi */
/* uint16_t e_type; // ET_EXEC, ET_DYN, ... */
/* uint16_t e_machine; // EM_X86_64 = 0x3E, ... */
/* uint32_t e_version; */
/* uint64_t e_entry; // virtual address of entry point */
/* uint64_t e_phoff; // file offset: program headers */
/* uint64_t e_shoff; // file offset: section headers */
/* // ... e_flags, e_ehsize, e_phentsize, ... 24 more bytes ... */
/* } Elf64_Ehdr; */
Two practical reminders:
e_ident index constants (EI_CLASS, EI_DATA, EI_VERSION, EI_OSABI) and the value constants (ELFCLASS64, ELFDATA2LSB, EM_X86_64) all come from <elf.h> — prefer them over magic numbers.lo | (hi << 8) for 16-bit, and so on up to 56 for 64-bit.Every Linux executable, shared library, and core dump begins with an ELF header.
The layout of those first bytes is fixed:
\x7fELF.This lesson is about reading the header only. We never modify it.
/* elf_header_reader.c — read-only ELF header triage tool.
* Build: cc -std=c11 -Wall -Wextra -o elfhdr elf_header_reader.c
* Run: ./elfhdr /bin/ls
*
* We parse the e_ident block BY HAND (no struct cast) so the logic is
* explicit and so it works regardless of the host's own byte order.
* This program never modifies the file: it opens it read-only.
*/
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <elf.h> /* EI_*, ELFCLASS*, ELFDATA*, EM_*, ET_* constants */
/* Read a 16-bit field from buf at off, honouring the file's byte order. */
static uint16_t rd16(const unsigned char *buf, size_t off, int little) {
return little
? (uint16_t)(buf[off] | (buf[off + 1] << 8))
: (uint16_t)((buf[off] << 8) | buf[off + 1]);
}
/* Read a 64-bit field from buf at off, honouring the file's byte order. */
static uint64_t rd64(const unsigned char *buf, size_t off, int little) {
uint64_t v = 0;
for (int i = 0; i < 8; i++) {
unsigned shift = little ? (unsigned)(i * 8) : (unsigned)((7 - i) * 8);
v |= (uint64_t)buf[off + i] << shift;
}
return v;
}
static const char *machine_name(uint16_t m) {
switch (m) {
case EM_X86_64: return "x86-64";
case EM_AARCH64: return "AArch64";
case EM_ARM: return "ARM";
case EM_386: return "x86";
default: return "unknown";
}
}
static const char *type_name(uint16_t t) {
switch (t) {
case ET_REL: return "REL (object file)";
case ET_EXEC: return "EXEC (fixed-address executable)";
case ET_DYN: return "DYN (shared object / PIE)";
case ET_CORE: return "CORE (core dump)";
default: return "unknown";
}
}
int main(int argc, char **argv) {
if (argc != 2) {
fprintf(stderr, "usage: %s <file>\n", argv[0]);
return 2;
}
int fd = open(argv[1], O_RDONLY); /* read-only: we never write */
if (fd < 0) { perror("open"); return 1; }
unsigned char buf[64];
ssize_t n = read(fd, buf, sizeof(buf));
if (n < 64) { /* refuse short reads up front */
fprintf(stderr, "not enough bytes for an ELF64 header (got %zd)\n", n);
close(fd);
return 1;
}
/* 1) Validate magic before trusting anything else. */
if (memcmp(buf, "\x7f" "ELF", 4) != 0) {
fprintf(stderr, "not an ELF file (bad magic)\n");
close(fd);
return 1;
}
/* 2) Read the single-byte identity fields (no endianness involved). */
int klass = buf[EI_CLASS]; /* 1=32-bit, 2=64-bit */
int data = buf[EI_DATA]; /* 1=LE, 2=BE */
if (klass != ELFCLASS64) {
fprintf(stderr, "this demo only parses ELF64 (class=%d)\n", klass);
close(fd);
return 1;
}
int little = (data == ELFDATA2LSB);
/* 3) Now multi-byte fields, interpreted in the file's byte order. */
uint16_t e_type = rd16(buf, 16, little);
uint16_t e_machine = rd16(buf, 18, little);
uint64_t e_entry = rd64(buf, 24, little);
uint64_t e_phoff = rd64(buf, 32, little);
uint64_t e_shoff = rd64(buf, 40, little);
/* 4) Bounds-check the in-file offsets against the real file size
* before any caller would trust them to seek. */
off_t size = lseek(fd, 0, SEEK_END);
if (size < 0) { perror("lseek"); close(fd); return 1; }
int phoff_ok = (e_phoff == 0) || (e_phoff < (uint64_t)size);
int shoff_ok = (e_shoff == 0) || (e_shoff < (uint64_t)size);
printf("Class: ELF64\n");
printf("Data: %s\n", little ? "little-endian" : "big-endian");
printf("Type: %s\n", type_name(e_type));
printf("Machine: %s (0x%X)\n", machine_name(e_machine), e_machine);
printf("Entry point: 0x%llx\n", (unsigned long long)e_entry);
printf("Phdr offset: %llu %s\n", (unsigned long long)e_phoff,
phoff_ok ? "(in bounds)" : "(OUT OF BOUNDS - suspicious)");
printf("Shdr offset: %llu %s\n", (unsigned long long)e_shoff,
shoff_ok ? "(in bounds)" : "(OUT OF BOUNDS - suspicious)");
close(fd); /* release the descriptor */
return 0;
}
What it does. It opens a file read-only, reads the first 64 bytes, refuses anything that is too short or lacks the ELF magic, decodes the identity bytes, then reads e_type, e_machine, e_entry, e_phoff, and e_shoff in the file's declared byte order, and finally bounds-checks the two offsets against the actual file size.
Expected output (run on a typical 64-bit Linux /bin/ls, a PIE binary):
Class: ELF64
Data: little-endian
Type: DYN (shared object / PIE)
Machine: x86-64 (0x3E)
Entry point: 0x6ab0
Phdr offset: 64 (in bounds)
Shdr offset: 140000 (in bounds)
The exact entry-point and section-header offset vary per binary; compare them against readelf -h /bin/ls.
Edge cases. A non-ELF file is rejected at the magic check. A 32-bit (ELFCLASS32) file is reported and skipped, because its field offsets differ — extending the tool to ELF32 means using the 52-byte layout. A truncated file (fewer than 64 bytes) is rejected before any field is read. A big-endian sample is handled correctly because rd16/rd64 honour data.
We trace the program on a normal 64-bit little-endian PIE executable.
open(..., O_RDONLY). We request read-only access. This is the single most important safety choice: the parser physically cannot modify the file.read(fd, buf, 64). We pull the first 64 bytes into a stack buffer. read returns the count actually read into n.n < 64). If the file is shorter than a 64-bit header, we stop now. Continuing would read uninitialised stack bytes — undefined behaviour and a classic info-leak bug.memcmp). We compare the first 4 bytes against 0x7F 'E' 'L' 'F'. Using memcmp (not a string function) is correct because the buffer is binary and the first byte is 0x7F.klass = buf[EI_CLASS] (byte 4). A single byte: 2 means ELFCLASS64. We require 64-bit here and bail otherwise. Single bytes have no endianness, which is exactly why we can read them before we know the byte order.data = buf[EI_DATA] (byte 5). 1 means little-endian; we set little = 1. Every multi-byte read below depends on this.rd16(buf, 16, little) → e_type. Bytes 16–17. For little-endian that is buf[16] | (buf[17] << 8). For a PIE this yields 3 (ET_DYN).rd16(buf, 18, little) → e_machine. Bytes 18–19. On x86-64 this is 0x3E.rd64(buf, 24, little) → e_entry. Bytes 24–31 combined into a 64-bit value. For a PIE this is a small file-relative address such as 0x6ab0 (it is added to the load base at runtime).rd64(buf, 32, little) / rd64(buf, 40, little) → e_phoff / e_shoff. The program- and section-header table file offsets. e_phoff is typically 64 (the headers immediately follow the ELF header).lseek(fd, 0, SEEK_END) → size. We learn the true file size, then check that each non-zero offset is strictly less than it. A crafted file with e_shoff past EOF would be flagged "OUT OF BOUNDS - suspicious" instead of trusted.close(fd). Report the fields, then release the descriptor. Always close what you open.| Step | Source bytes | Field | Example value |
|---|---|---|---|
| 5 | 0–3 | magic | 7F 45 4C 46 (valid) |
| 6 | 4 | EI_CLASS | 2 → ELF64 |
| 7 | 5 | EI_DATA | 1 → little-endian |
| 8 | 16–17 | e_type | 3 → ET_DYN |
| 9 | 18–19 | e_machine | 0x3E → x86-64 |
| 10 | 24–31 | e_entry | 0x6ab0 |
| 11 | 32–39 | e_phoff | 64 |
Wrong:
uint16_t e_machine = *(uint16_t *)(buf + 18); /* uses HOST endianness */
On a little-endian laptop reading a big-endian ARM binary, this produces a byte-swapped, nonsense value. It also dereferences a possibly-misaligned pointer, which is undefined behaviour on some platforms.
Why wrong. The file declares its own byte order in e_ident[EI_DATA]. Your host's order is irrelevant to what the file means.
Corrected:
int little = (buf[EI_DATA] == ELFDATA2LSB);
uint16_t e_machine = rd16(buf, 18, little); /* honours the file */
Prevent/recognise it. If your parser works on local binaries but disagrees with readelf on a foreign-arch sample, suspect endianness.
Elf64_Ehdr without checking classWrong:
Elf64_Ehdr *h = (Elf64_Ehdr *)buf; /* assumes 64-bit */
uint64_t entry = h->e_entry;
For a 32-bit (ELFCLASS32) file the layout is the 52-byte Elf32_Ehdr; the 64-bit field offsets are wrong, so entry reads the wrong bytes. The cast may also be unaligned.
Corrected: check buf[EI_CLASS] == ELFCLASS64 first, then use the matching layout (or parse by hand as in the lesson).
Wrong: if (strncmp(buf, "\x7fELF", 4) == 0) — the C escape \x7fELF is ambiguous (the compiler may read more hex digits into \x7f), and strncmp stops at a NUL. Binary data is not a string.
Corrected: if (memcmp(buf, "\x7f" "ELF", 4) == 0) — the split string literal makes the bytes unambiguous, and memcmp compares an exact length with no NUL assumption.
Wrong: lseek(fd, ehdr.e_shoff, SEEK_SET); read(fd, sh, sizeof(sh)); with no validation. A malformed file sets e_shoff past EOF (short read mishandled) or to a huge value.
Corrected: verify e_shoff != 0 && e_shoff + needed <= file_size before seeking, and treat a failed check as a malformed/suspicious file.
Compiler errors.
error: unknown type name 'Elf64_Ehdr' → you forgot #include <elf.h>.error: 'EM_AARCH64' undeclared → same fix; the constants live in <elf.h> too.%llx vs uint64_t → cast to (unsigned long long) when printing 64-bit values portably, as the example does.Runtime errors.
open: No such file or directory → wrong path; pass a real binary like /bin/ls.read returns fewer bytes; loop until you have 64 bytes (or use pread).e_phoff/e_shoff; add the bounds check.Logic errors.
0x3E00) → you read with the wrong endianness; recheck little.e_type prints unknown for a normal program → you may be reading from the wrong offset; remember e_type is bytes 16–17, not 14–15.Cross-check against ground truth.
readelf -h /bin/ls # full parsed header — the authoritative reference
file /bin/ls # one-line summary (class, machine, PIE, stripped...)
xxd -l 64 /bin/ls # raw first 64 bytes, to eyeball your offsets
Diff your tool's output line by line against readelf -h. If they disagree, ask: am I reading the right offset? am I honouring EI_DATA? did I check EI_CLASS?
Questions to ask when it doesn't work. Did the magic match? What does EI_CLASS/EI_DATA say? Are my field offsets correct for ELF64? Am I building multi-byte values in the file's byte order? Did I read all 64 bytes?
Parsing an untrusted binary means every byte is attacker-controlled. The header itself is a parser-attack surface, so apply defensive practices throughout.
Bounds and short reads. Never act on fewer bytes than a field needs. The example refuses any read under 64 bytes before touching a field; reading past the end of buf is a buffer over-read and undefined behaviour, and on a partially-initialised stack buffer it can leak unrelated memory into your output.
Never trust in-file offsets. e_phoff and e_shoff come from the file. Validate offset + element_size <= file_size before any lseek/read or buffer index. An unchecked offset is the classic way a crafted ELF turns a parser into an out-of-bounds read or a crash (CVE history for binary parsers is full of exactly this).
Integer overflow. When computing offset + count * size for a table, do the arithmetic in a wide unsigned type and check it does not wrap. A malicious e_shnum/e_shentsize multiplied naively can overflow and slip past a bounds check.
Alignment. Do not cast unsigned char *buf + N directly to a wider pointer and dereference it; the address may be misaligned for the target type, which is undefined behaviour on strict-alignment CPUs. Reading byte-by-byte (as rd16/rd64 do) sidesteps this entirely.
Read-only, no execution. Open with O_RDONLY. Never mmap an untrusted file as executable, never run the sample. Analysis happens on bytes, not on a running process.
Authorization. Practise only on binaries you are authorised to inspect. Handle suspicious samples solely inside an isolated, authorised lab (a VM/container you own, no network egress), never on a production or shared host.
Detection & logging. When a tool rejects a file, log what failed — bad magic, short file, out-of-bounds offset, unexpected class/machine — plus a file hash (e.g. SHA-256) and the path, so analysts can correlate. Never log file contents that might contain secrets, and never log credentials or tokens.
Verifying a mitigation. To confirm your bounds check works, craft a local test fixture (not real malware): copy a benign ELF, set e_shoff to a value larger than the file using a hex editor on your own copy, and confirm your tool reports "OUT OF BOUNDS - suspicious" and does not crash. That is mitigation verification — proof the defence triggers.
Where this shows up.
file(1), readelf, objdump, nm, and every reverse-engineering framework (Ghidra, radare2) begin by parsing this header.e_machine matches the target platform before shipping or installing it.e_type/e_ident to confirm artifacts are the expected kind (PIE, stripped, correct ABI).Professional best-practice habits.
Beginner rules:
EI_CLASS and EI_DATA before any multi-byte field.O_RDONLY; never execute a sample.<elf.h> constants instead of magic numbers, and meaningful names.Advanced habits:
1. Magic detector. Write a program that takes a file path, reads the first 4 bytes, and prints ELF or not ELF.
O_RDONLY, handle a file shorter than 4 bytes, use memcmp../magic /bin/ls → ELF; ./magic /etc/hostname → not ELF.2. Class and data reader. Extend task 1 to also print whether the file is 32- or 64-bit and little- or big-endian.
buf[EI_CLASS] and buf[EI_DATA] using the <elf.h> constants.ELF64, little-endian.e_ident, EI_CLASS, EI_DATA.3. Machine name printer. Read e_machine (bytes 18–19) honouring the file's byte order and print a human-readable name for x86-64, AArch64, ARM, and x86, else unknown (0x..).
rd16 helper that takes a little flag; do not use a struct cast.readelf -h <file> line Machine:.e_machine.4. Offset bounds-checker. Read e_phoff and e_shoff (ELF64) and report, for each, whether it lies within the actual file size.
lseek(fd, 0, SEEK_END); treat offset 0 as 'absent, ok'; flag anything >= size as suspicious.e_shoff you enlarged with a hex editor, print Shdr offset: ... (OUT OF BOUNDS - suspicious).5. Mini readelf -h (ELF32 + ELF64). Combine everything into one tool that prints Class, Data, Type, Machine, Entry, Phdr offset, and Shdr offset for both 32- and 64-bit ELF files, choosing the correct layout from EI_CLASS.
O_RDONLY.e_entry/e_phoff/e_shoff are 32-bit and at different offsets than ELF64 — derive them from the 52-byte layout.e_ident handling, layout selection, endianness, bounds checks, robust error handling. Do not just shell out to readelf — implement the parsing yourself.0x7F 'E' 'L' 'F'.e_ident first: EI_CLASS (32- vs 64-bit) tells you the layout, and EI_DATA (LE vs BE) tells you how to interpret every multi-byte field. These are single bytes with no endianness of their own.e_type (REL/EXEC/DYN/CORE — modern executables are usually DYN/PIE), e_machine (target CPU), e_entry (virtual address of first instruction), and e_phoff/e_shoff (file offsets of the header tables).e_entry) from a file offset (e_phoff/e_shoff) — never seek to an address.O_RDONLY, never execute a sample, and analyse suspicious files only in an isolated, authorised lab. Cross-check your work with readelf -h and file(1).