rea-reverse

ELF, PE and Mach-O Viewer Online: Is Your Binary Stripped?

Written for REA 6.4.0Updated 9 min read
On this page

Short answer: the first four bytes of a native binary tell you its format. 7F 45 4C 46 is ELF (Linux, Android, most firmware), 4D 5A (“MZ”) followed by a PE\0\0 header is PE (Windows EXE, DLL, SYS), and CF FA ED FE or CE FA ED FE is Mach-O (macOS, iOS). To see everything else, drop the file into this site’s native binary overview. It shows the architecture, file type, entry point and linked libraries, the sections with their entropy, the imports, exports and strings, and for ELF whether the binary is stripped. It runs in your browser and nothing is uploaded or executed.

Who this is for: developers who received a .so, .dll or a binary without an extension and want to know what it is before they ship, link or debug it. It is also for release engineers who need to confirm that a build is stripped (or that it still carries symbols for crash reports).

What you will answer:

  • Is this ELF, PE or Mach-O, and for which CPU?
  • What does it link against, and what does it export?
  • Which sections does it have, and does any of them look compressed?
  • Does it still carry a symbol table, or was it stripped?
  • Which command-line tools give the same answers?

Samples used: one small C program of my own, built on macOS for three platforms. ELF and PE come from the Android NDK r29 toolchain (clang and lld-link), Mach-O from Xcode’s clang:

#include <stdio.h>
#define HELLO_VERSION "hello 1.2.0"
int add(int a, int b) { return a + b; }
int main(void) { printf("%s: %d\n", HELLO_VERSION, add(2, 3)); return 0; }
B=$ANDROID_HOME/ndk/29.0.14206865/toolchains/llvm/prebuilt/darwin-x86_64/bin
$B/aarch64-linux-android29-clang -O2 -o hello-arm64-android hello.c
$B/llvm-strip -o hello-arm64-android.stripped hello-arm64-android
clang -O2 -arch arm64 -mmacosx-version-min=12.0 -o arm64 hello.c
clang -O2 -arch x86_64 -mmacosx-version-min=10.15 -o x86_64 hello.c
lipo -create arm64 x86_64 -output hello-macos-universal

The Windows build (hello-x64.exe) is a similar program built without the C runtime and linked with lld-link against kernel32.dll only. Use the same steps on your own builds or on open-source binaries. If you go on to run or attach to software you did not build, make sure you are authorized to, and read REA’s DISCLAIMER.md.

Step 1: Tell the three formats apart

All three formats start with a fixed “magic” value, so you can identify a file even without an extension:

First bytes Format Notes
7F 45 4C 46 (\x7fELF) ELF Byte 4 is the class (1 = 32-bit, 2 = 64-bit), byte 5 the byte order
4D 5A (MZ) PE The DOS header points (at offset 0x3C) to the real PE\0\0 header
CF FA ED FE / CE FA ED FE Mach-O 64-bit / 32-bit, little-endian (the byte-swapped forms are big-endian)
CA FE BA BE Universal Mach-O or Java class The same magic is used by both

The last row catches people out. A universal (“fat”) Mach-O stores the number of architectures right after the magic, while a Java .class file stores its version there. The viewer applies the same rule as file(1): more than 30 “architectures” means it is not a universal binary. I compiled a one-line Hello.java with javac (its header is cafe babe 0000 003d), dropped the class file in, and the tool refused it with “Not an ELF, PE or Mach-O file.” and the technical detail “cafebabe magic but not a universal binary (Java class file?)”.

Step 2: Read the identity block

Open the file in the native binary overview. The Identity block comes first. These are the values it reported for three of my samples:

Field hello-arm64-android hello-x64.exe hello-macos-universal (slice 1)
Format ELF64 PE32+ Mach-O 64-bit
Architecture AArch64 x64 (0x8664) arm64
Bitness 64-bit 64-bit 64-bit
File type ET_DYN EXE MH_EXECUTE
Role executable executable executable
Entry point 0x4650 RVA 0x1010 (VA 0x140001010) entryoff 0x468 (VA 0x100000468)
Interpreter /system/bin/linker64 — /usr/lib/dyld
Format-specific OS/ABI UNIX - System V Subsystem WINDOWS_CUI, linker 14.0 macOS, minimum OS 12.0, SDK 27.0, UUID

A few things are worth knowing when you read this block:

  • ET_DYN does not always mean “shared library”. PIE executables are ET_DYN too. The tool calls the file an executable when it has a PT_INTERP interpreter or the DF_1_PIE flag, which is how my Android binary got the role executable.
  • The interpreter tells you the platform. /system/bin/linker64 is Android’s 64-bit dynamic linker, while a desktop Linux binary would name something like ld-linux-x86-64.so.2.
  • Universal binaries have slices. For hello-macos-universal the tool lists two slices, x86_64 (12,592 bytes at offset 4096) and arm64 (33,464 bytes at offset 32768), and you pick the one to analyze. lipo -detailed_info and REA report the same offsets (see Step 6).

When a PE file carries a CLI header, the identity block also says “This is a .NET assembly” and links to the .NET metadata and IL viewer. For managed code, the PE headers alone do not tell the whole story; Check a .NET DLL’s Target Framework covers the managed side.

Step 3: Sections, segments and entropy

The Sections table lists every section with its address, file offset, size, permissions and Shannon entropy in bits per byte. For the AArch64 build, .text (the code) was 308 bytes at 5.53 bits per byte, .rodata 20 bytes at 3.82 and .dynstr 73 bytes at 4.21. On Windows, hello-x64.exe has just three sections: .text, .rdata and .pdata.

Entropy is a rough measure of how random the bytes look. In my samples, code sections landed between about 5.2 and 6.8 bits per byte, while string and symbol tables sat lower. The tool flags a section as high entropy when it is at least 1 KiB and above 7.2 bits per byte, which usually means compressed or encrypted data: embedded archives, images, compressed resources or packed code. None of my samples came close (the largest, Newtonsoft.Json’s .text, was 5.93).

A high-entropy section is a fact about the bytes, not a verdict. Installers, games and apps with bundled assets often have them for ordinary reasons. Use it to decide where to look next.

ELF files also have segments (program headers): the PT_LOAD ranges the loader maps, plus PT_INTERP, PT_DYNAMIC, PT_GNU_STACK and PT_GNU_RELRO. Sections are for linkers and debuggers, segments are what the loader actually uses. That is why a binary can run without any section headers at all.

Step 4: Libraries, imports, exports and strings

Linked libraries answers “what does this need at runtime?”:

  • ELF: the DT_NEEDED entries. My Android build needs libdl.so and libc.so.
  • PE: the import directory. hello-x64.exe imports from kernel32.dll only.
  • Mach-O: LC_LOAD_DYLIB and friends. The macOS build loads /usr/lib/libSystem.B.dylib.

Imports and exports lists symbol names. For ELF the tool reads .dynsym, the dynamic symbol table the loader uses, and only falls back to .symtab when there is no .dynsym (object files and static executables). So my Android executable shows four imports (printf, __libc_init, __cxa_atexit, __register_atfork) and no exports, even before stripping: main and add live only in .symtab. A PE DLL lists its export table; my add-x64.dll exports exactly one function, add.

Strings finds ASCII and UTF-16LE runs of at least six characters (adjustable) and names the section each one sits in. The version string hello 1.2.0 showed up in .rodata (ELF), .rdata (PE) and __TEXT,__cstring (Mach-O). That is the quickest way to confirm which version of your library ended up in a release package. The full list can be downloaded as a .txt file.

Step 5: Is the binary stripped?

“Stripped” means the full symbol table, which debuggers and profilers use to name functions, was removed. The dynamic symbols needed for linking stay. How you check depends on the format.

ELF: look for .symtab

The Security features table has a row Symbol table stripped. It reads no when a .symtab section exists and yes when there is no .symtab or no section headers at all. On my sample, before and after llvm-strip:

hello-arm64-android hello-arm64-android.stripped
Symbol table stripped no (.symtab 38 entries) yes (no .symtab)
Sections 26 23 (.comment, .symtab, .strtab removed)
File size 6,088 bytes 4,248 bytes
Imports 4 4

The import count does not change, because .dynsym survives stripping. Standard tools agree:

$ file hello-arm64-android hello-arm64-android.stripped
hello-arm64-android:          ELF 64-bit LSB pie executable, ARM aarch64, ... not stripped
hello-arm64-android.stripped: ELF 64-bit LSB pie executable, ARM aarch64, ... stripped

$ llvm-nm hello-arm64-android.stripped
hello-arm64-android.stripped: no symbols
$ llvm-nm -D hello-arm64-android.stripped
                 U __cxa_atexit@LIBC
                 U __libc_init@LIBC
                 U __register_atfork@LIBC
                 U printf@LIBC

On Android, release builds are usually stripped by the Gradle build. Keep the unstripped .so files (or their build IDs) so you can symbolize native crash reports later.

Mach-O and PE: no single flag

The tool has no “stripped” row for Mach-O or PE, because neither format has one obvious marker. You can still judge it from the symbol counts:

  • Mach-O: the tool reads the LC_SYMTAB nlist table. Before strip, my arm64 executable showed 3 exports (__mh_execute_header, _add, _main) and 1 import (_printf). After strip, only __mh_execute_header and _printf were left, which matches nm. strip also re-applied an ad hoc signature, so the Signing identifier field changed to the new file name.
  • PE: MSVC-style toolchains (including lld-link) keep debug symbols in a separate .pdb file, so the image itself carries only the import and export tables, which stripping does not touch. The tool does not read the debug directory, so it cannot tell you whether a PDB was produced. MinGW builds are different: they can carry DWARF sections such as .debug_info in the image, and those appear in the section list until you strip them.

Step 6: The same from the command line, and with REA

Every value above can be cross-checked with standard tools. I ran these on the same files:

$ llvm-readelf -h hello-arm64-android | grep -E "Class|Type|Machine"
  Class:                             ELF64
  Type:                              DYN (Shared object file)
  Machine:                           AArch64

$ llvm-readobj --file-headers hello-x64.exe | grep -E "Machine|Magic"
  Machine: IMAGE_FILE_MACHINE_AMD64 (0x8664)
  Magic: 0x20B

$ otool -hv hello-macos-arm64
      magic  cputype cpusubtype  caps    filetype ncmds sizeofcmds      flags
MH_MAGIC_64    ARM64        ALL  0x00     EXECUTE    17       1056   NOUNDEFS DYLDLINK TWOLEVEL PIE

$ lipo -info hello-macos-universal
Architectures in the fat file: hello-macos-universal are: x86_64 arm64

readelf, objdump, nm and strip come with binutils on Linux, and the llvm-* equivalents ship with the Android NDK and with LLVM. otool, lipo and codesign need the Xcode command line tools on macOS.

REA, the open-source project by morluto, adds structured JSON for agents and scripts. In 6.4.0, rea list-architectures <path> and rea inspect-macho <path> read Mach-O files, but they wrap Apple’s command-line tools (file, lipo, otool, nm, dyld_info and others) and so only run on macOS with the Xcode command line tools. On my universal binary, list-architectures returned both slices with the same offsets and sizes as the browser tool, plus their alignment:

[{"name":"x86_64","cpu_type":"CPU_TYPE_X86_64","cpu_subtype":"CPU_SUBTYPE_X86_ALL","file_offset":4096,"size":12592,"alignment":4096},
 {"name":"arm64","cpu_type":"CPU_TYPE_ARM64","cpu_subtype":"CPU_SUBTYPE_ARM64_ALL","file_offset":32768,"size":33464,"alignment":16384}]

inspect-macho is upfront about stripping too. Its limitations line says “Imports and exports combine dyld_info and nm; stripped or toolchain-hidden symbols may be absent.”

For ELF and PE, REA 6.4.0 has no header viewer that matches the browser tool. inspect-binary-layout reports sections, symbols and mitigations, but its initial profile only covers ELF64 x86-64 files, only on Linux x64, with pwntools 4.15.0 that you install yourself. For reading the code itself, the next step is REA with Ghidra: Free Native Binary Analysis with REA and Ghidra walks through decompiling, string search and cross-references on a binary you build.

FAQ

Is my file uploaded anywhere? No. The binary overview parses the file in a Web Worker in your browser that has no network access. Nothing is run, disassembled or uploaded.

How large a file can I open? Up to 512 MiB. Very large tables are capped (for example, 4,096 sections and 100,000 symbols), and the report notes it when that happens.

Can it open .so files from an APK? Yes. Unzip the APK and open any file from lib/<abi>/. For checking those libraries against Android’s 16 KB page size requirement, see Check an APK for Android’s 16 KB Page Size Requirement, or open the whole APK in the APK analyzer.

Does a “yes” in Security features mean the binary is safe? No. Those rows describe how the file was built (PIE, NX, RELRO, signatures present). Signatures are detected, never verified.

What about core dumps and object files? ELF core dumps (ET_CORE) and object files (ET_REL, MH_OBJECT) are recognized and labeled by role. They have no entry point or dynamic linking information to show.

Next steps