Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

Assembly (x86-64, AArch64, RISC-V)  Module 15 – Security at the machine level (defensive)

Reverse engineering binaries you built

Take apart a stripped program you built: identify the file and its entry, read its imports and strings, then recover its loop and prove the reading by output.

  • Advanced
  • 15 minutes
  • Examples run with Python 3.14.8
  • By MySmartCoPilot
Free core, Pro depth: 6 of 15 minutes free
Free: Read only what you may, then work from the outside in · Identify the file, its processor and where it starts · Imports and strings are the clues a stripped file keeps · Practise. With a pass: 2 in-depth parts and 3 interview answers, about 9 minutes.

What you will learn

  • Identify a stripped binary's format, processor and entry point
  • Read a binary's imports and strings before its instructions
  • Decode a function's constants and loop from its stripped listing
  • Explain what stripping removes from a binary and what it leaves
  • Choose between a listing, a decompiler and a debugger for a question

Before you start

What you’ll learn · Before you start

What you will learn

  • Identify a stripped binary's format, processor and entry point
  • Read a binary's imports and strings before its instructions
  • Decode a function's constants and loop from its stripped listing
  • Explain what stripping removes from a binary and what it leaves
  • Choose between a listing, a decompiler and a debugger for a question

Before you start

On this page

You built a small tool, shipped the release build, and lost the source. The binary still runs, but nm on the stripped copy names no function of your own, only the two it imports, printf and strlen, and its one function sits at a bare offset 0x460, 1120 bytes into the file. Recovering what that function computes, exactly, is reverse engineering: Recovering how a compiled program works from the program itself when its source is not available: its format, where it starts, what it calls and what its functions compute. Whether you may do it depends on who owns the program and on its licence. a binary you built: reading the compiled file to recover its behaviour once the source is gone.

This page takes apart one stripped program in a fixed order that reads the cheap clues before the dear ones: check that you may study it, identify the file, find where it starts, read its imports and strings, and only then read instructions.

Read only what you may, then work from the outside in

The first question is not a tool but permission. A binary you compiled, or one built for you to study, is yours to take apart; a third party’s program is not, unless its licence and the law allow it, and such allowances are narrow. United States law, for one, lets a lawful user get around an access control only to identify what an independently written program needs to work with it, and only when that was not already available. Settle that first.

Interactive · stage by stage Taking apart a binary you built Follow the stripped checksum program through each step, in the order that wastes the least reading.
Taking apart a binary you built, step 6 of 6. Showing may you study it?, identify the file, find the entry, imports and strings, read the code, prove it by output. In focus: prove it by output.may you study it?built by you, or for youidentify the fileformat, processor, flagsfind the entryLC_MAIN or e_entryimports and stringswhat it calls and printsread the codebetween the known callsprove it by outputsame lines, many inputs

The drawing shows the whole scene; its 6 steps are listed below.

Every step as text
  1. First, the scope. Only a binary you built, or one built for you to study, is in scope here. That question comes before any tool, because licences and the law can forbid the rest.
  2. What kind of file. file and otool -hv say what it is before any instruction is read: a Mach-O executable for arm64, position independent. On Linux, readelf -h answers the same question for an ELF file.
  3. Where it starts. The header names the first instruction to run. LC_MAIN gave offset 1120, 0x460, which is where the stripped file's only function begins; an ELF file names it in e_entry.
  4. The clues it cannot hide. Imports keep their names because the loader binds them by name: this program calls strlen and printf. Its strings add a default word and a format that prints eight hex digits.
  5. Then the code. Only now read instructions, starting from the known calls: what feeds strlen, what reaches printf, and the loop between them.
  6. Proof, not a guess. A reading becomes a result once a reimplementation prints the same lines as the original on inputs chosen to catch mistakes.

The order matters because each step narrows the next: the file’s type picks the tools, the entry point says where the code begins, and the imports and strings name the landmarks to read the instructions around.

Identify the file, its processor and where it starts

file and otool -hv answer what the thing is before an instruction is read; the header’s last word, PIE, marks a position-independent executable: An executable whose code reaches other code and data through PC-relative addressing instead of fixed addresses, so it runs correctly at any load address. This is what lets the loader randomise its placement under ASLR. More in “Memory and a process's address space”, whose load address changes from run to run. otool -l then prints the LC_MAIN load command, whose entryoff is the offset of the first instruction to run; an ELF: The Executable and Linkable Format of the generic System V ABI, used by Linux and most Unix systems. It has a section header table for the linker and a program header table for the loader. More in “From source to executable: objects and linking” file names its entry as an address, e_entry, which readelf -h prints.

checksum.c: the program this page builds, strips and then recovers C · checksum.c
/* The program this page takes apart: it prints a 32-bit checksum of a word, or of "kaksha" when given none. The page
   builds it, strips it, and then works only from the stripped file, as if this source were lost. */
#include <stdio.h>
#include <string.h>

static unsigned int mix(unsigned int h, unsigned char c) {
    h ^= c;
    h *= 16777619u;
    return h;
}

static unsigned int checksum(const char *s) {
    unsigned int h = 2166136261u;
    for (size_t i = 0; i < strlen(s); i++) {
        h = mix(h, (unsigned char)s[i]);
    }
    return h;
}

int main(int argc, char **argv) {
    const char *word = (argc > 1) ? argv[1] : "kaksha";
    printf("%s -> %08x\n", word, checksum(word));
    return 0;
}
Identify the stripped file, its header flags, its entry offset, and what stripping took Python · identify_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

session.run("""
# Build the program and keep a stripped copy; from here on, only the stripped file is read
clang -O2 -o checksum checksum.c
strip -o checksum_stripped checksum
# What kind of file it is, for which processor, and the header flags the loader reads
file checksum_stripped
otool -hv checksum_stripped | tail -1
# Where it starts: LC_MAIN gives main's offset in the file, which is where the reading begins
otool -l checksum_stripped | grep -A3 LC_MAIN
# What stripping took: the name main is gone, while the two imports stay because the loader binds them by name
nm checksum
nm checksum_stripped
""")

Output

# Build the program and keep a stripped copy; from here on, only the stripped file is read
$ clang -O2 -o checksum checksum.c
$ strip -o checksum_stripped checksum
# What kind of file it is, for which processor, and the header flags the loader reads
$ file checksum_stripped
checksum_stripped: Mach-O 64-bit executable arm64
$ otool -hv checksum_stripped | tail -1
MH_MAGIC_64    ARM64        ALL  0x00     EXECUTE    17       1056   NOUNDEFS DYLDLINK TWOLEVEL PIE
# Where it starts: LC_MAIN gives main's offset in the file, which is where the reading begins
$ otool -l checksum_stripped | grep -A3 LC_MAIN
       cmd LC_MAIN
   cmdsize 24
  entryoff 1120
 stacksize 0
# What stripping took: the name main is gone, while the two imports stay because the loader binds them by name
$ nm checksum
0000000100000000 T __mh_execute_header
0000000100000460 T _main
                 U _printf
                 U _strlen
$ nm checksum_stripped
0000000100000000 T __mh_execute_header
                 U _printf
                 U _strlen

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 identify_session.py

Stripping removed the name _main from the symbol table, so nm on the stripped file lists only the header symbol and the two undefined imports. The entry offset 1120, which is 0x460 in hex, is where the reading begins, and the header already names the file: an arm64 Mach-O executable whose code starts there. Nothing so far has read an instruction.

Checkpoint

Check: Who may study a binary, and the order of work · The file, its processor and entry point

  1. Question 1 of 2 A colleague sends you a competitor's program and asks you to take it apart to copy a feature. What is the first thing to establish?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: Whether you are allowed to study it at all, before any tool

    The first question is permission, not a tool. A binary you compiled or were given to study is in scope; picking apart a third party's program to copy it is not, and the legal exceptions are narrow and about interoperability.

  2. Question 2 of 2 On the stripped file, otool -l shows LC_MAIN with entryoff 1120. What does that tell you?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: The program's first instruction is at file offset 1120, which is 0x460

    LC_MAIN's entryoff is the offset of the first instruction, 1120 (0x460). An ELF file gives the same answer in its header's e_entry.

Imports and strings are the clues a stripped file keeps

A stripped file still names what it imports, because the loader binds each import by name when the program starts; the code reaches each one through a symbol stub: A few instructions the Mach-O linker writes into the __TEXT,__stubs section for each imported function. On arm64 a stub loads the function's address from its __got slot into x16 and branches there. More in “Calling C library functions from assembly”. otool -Iv lists them, and strings prints the readable byte sequences, by default runs of at least four printable characters.

The imports every stripped file keeps, the strings, and the program run on inputs you choose Python · clues_session.py
import pathlib
import sys

sys.path.insert(0, str(pathlib.Path(__file__).parent / "sessions"))
import session

session.run("""
# The same stripped build as before
clang -O2 -o checksum checksum.c
strip -o checksum_stripped checksum
# The imports: every library call goes through a stub, and the stripped file still names each one
otool -Iv checksum_stripped
# The strings: a word, and a format that prints a word, an arrow and eight hex digits
strings checksum_stripped
# Run it on inputs you choose: no word, two words one letter apart, and the empty word
./checksum_stripped
./checksum_stripped hello
./checksum_stripped hellp
./checksum_stripped ''
""")

Output

# The same stripped build as before
$ clang -O2 -o checksum checksum.c
$ strip -o checksum_stripped checksum
# The imports: every library call goes through a stub, and the stripped file still names each one
$ otool -Iv checksum_stripped
checksum_stripped:
Indirect symbols for (__TEXT,__stubs) 2 entries
address            index name
0x00000001000004e0     2 _printf
0x00000001000004ec     3 _strlen
Indirect symbols for (__DATA_CONST,__got) 2 entries
address            index name
0x0000000100004000     2 _printf
0x0000000100004008     3 _strlen
# The strings: a word, and a format that prints a word, an arrow and eight hex digits
$ strings checksum_stripped
kaksha
%s -> %08x
# Run it on inputs you choose: no word, two words one letter apart, and the empty word
$ ./checksum_stripped
kaksha -> ca60f7d4
$ ./checksum_stripped hello
hello -> 4f9f2cab
$ ./checksum_stripped hellp
hellp -> 5c9f4122
$ ./checksum_stripped ''
 -> 811c9dc5

Recorded with Python 3.14.8 on macOS 26 arm64. To run it yourself: mise exec python@3.14.8 -- python3 clues_session.py

Two imports, printf and strlen, and two strings, a default word and a format %s -> %08x, already sketch the program: it measures a string, folds it into a 32-bit value and prints that as eight hex digits. Running it confirms the shape: the empty word gives a fixed starting value 811c9dc5, and hello and hellp, one letter apart, differ in six of their eight hex digits, as a hash’s results do. The code between those calls comes next, read from the listing and proved by output. A listing shows every instruction exactly, a decompiler: A program that reads a binary's machine code and rebuilds C-like source from it, so that a function can be read as statements and loops rather than as instructions. Ghidra is one widely used example. rebuilds C-like source from the bytes, and a debugger shows real values as the program runs.

Checkpoint

Check: The imports and strings a stripped file keeps

  1. Question 1 of 2 The stripped file's nm output lost _main, yet otool -Iv still lists _printf and _strlen. Why do the imports survive stripping?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: The dynamic loader binds them by name at run time, so the names must stay in the file

    strip removes a program's own symbols, such as _main, but not the names it imports: the loader binds those by name when the program starts, so the file needs them to run.

  2. Question 2 of 2 Running the stripped file on "hello" and "hellp" gives 4f9f2cab and 5c9f4122. What does that confirm before you read any instruction?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: One changed letter changes most of the output, six of the eight hex digits here, which is how a hash behaves

    Running on chosen inputs confirms the shape the strings suggested: a hash, where one changed byte changes most of the result, and the empty word returns the fixed starting value.

Practise

Work on your own copies of checksum.c and the sessions above; for a machine other than macOS on Apple silicon, see setting up the toolchain.

  1. Build and strip checksum.c, then read the entry offset from otool -l and predict which section 0x460 falls in before you disassemble.
  2. Run the stripped program on a word and on the same word with one letter changed, and count how many of the eight hex digits differ.
  3. Add a third string to checksum.c, rebuild and strip, and find it with strings; note that _main stays gone.
  4. Run otool -Iv on the stripped file and match each stub address to the import it binds.

Practice that adapts to you

Practise this lesson

6 questions about 7 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 Which binary is clearly in scope to take apart however you like?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: One you compiled yourself from your own source

    A binary you built, or one given to you to study, is in scope. A third party's program, especially to copy it, is not, and the exceptions that exist are narrow and aimed at interoperability.

  2. Question 2 of 6 Why read a stripped binary from the outside in, headers and imports before instructions?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: Each step names landmarks that make the next one quicker; instructions read fastest once you know the calls and strings

    Reading cheap clues first means that by the time you reach instructions you know the file type, the entry point and the calls and strings, so the instructions have landmarks around them.

  3. Question 3 of 6 Which command and field name a Mach-O program's entry point?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: otool -l, the LC_MAIN load command's entryoff

    LC_MAIN carries entryoff, the offset of the first instruction. On ELF the same answer is e_entry in the header, read with readelf -h.

  4. Question 4 of 6 otool -hv ends its flags with PIE. What does that tell you about the addresses in the listing?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: The code is position independent, so addresses are offsets from wherever it loads, not fixed

    PIE marks a position-independent executable: the loader places it at a run-time base and the addresses you read are offsets from that base.

  5. Question 5 of 6 What does otool -Iv list for the stripped checksum file?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: Its imported functions, _printf and _strlen, each with a stub address

    otool -Iv prints the indirect symbols, the imported library functions, here _printf and _strlen, which a stripped dynamically linked file keeps so the loader can bind them.

  6. Question 6 of 6 strings on the stripped file prints "kaksha" and "%s -> %08x". Before reading any instruction, what do these suggest?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: A default word and a format that prints a word, an arrow and eight hex digits, so the program hashes a string and prints it

    The format %s -> %08x prints text, an arrow and an eight-digit hex number, and "kaksha" is the default input, so the program computes and prints a 32-bit value from a word before you read a single instruction.

Recovering the checksum's loop from the listing, and proving it by output Pro: opens with a Pro, Premium or Ultimate pass

The imports and strings said the program hashes a string; this part reads the one function to recover how, then checks the reading the only way that settles it, by output. The listing is short because the function is small, and every constant in it can be put back together by hand.

In this part:

  • Rebuilding the loop’s two constants from their halves
  • Proving the reading by output, and the misreading it catches

1 diagram · 2 runnable examples (Python) · about 8 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

What stripping leaves behind, and which way of reading fits the question Pro: opens with a Pro, Premium or Ultimate pass

Stripping is sold as making a binary hard to read, so it helps to know exactly what it removes and what it cannot. The answer depends on how the program was linked, and it changes which tool earns its place.

In this part:

  • What a stripped file keeps: dynamic imports versus static calls
  • Choosing a listing, a decompiler or a debugger

1 runnable example (Python) · about 8 minutes

Opening this part with your pass…Pro part. It opens with the same pass as the part above.

Interview questions

A warm-up for freshers: you are handed a stripped binary your team built and asked what one function does. What do you look at first, before any disassembly?

Before any disassembly, read the file from the outside in, cheapest clue first. file and otool -hv, or readelf -h for an ELF file, give the format, the processor and whether the code is position independent, and the entry load command gives the offset where the code begins. Then otool -Iv lists the library functions it imports, which a stripped file keeps because the loader binds them by name, and strings prints its readable text. Those landmarks usually sketch what the function does before you read a single instruction, and they tell you which instructions are worth reading closely.

Three harder questions follow: recovering a lost function exactly, the scope of work on someone else’s binary, and what stripping a release build really buys.

  1. Mid to senior

    Your team lost the source of a small internal tool, and only its stripped release binary remains. How would you recover exactly what its main function computes, and how would you show that your recovery is right?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

  2. Senior to staff

    A product manager asks you to take apart a partner's closed-source library to learn its file format, so that your app can read the partner's files. How do you decide whether to do it, and what do you do first?

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A teammate proposes stripping the release build so that nobody can see how the app works. What does stripping actually remove, what does it leave, and what would you tell them?

    Model answer, Pro. First step and follow-ups

Flashcards · 5 cards

Recall it, then check

Say the answer to yourself before you look: recalling it is what makes it stick.

  1. What is reverse engineering a binary you built, and where does it start?

    Reading the compiled file to recover what it does when the source is not at hand. It starts with whether you are allowed to read it: that depends on who owns the program and its licence.

  2. In what order should you read an unfamiliar binary?

    From the outside in: identify the file and its processor, find its entry, then read its imports and strings, so you know the landmarks before you read any instructions.

  3. Which commands give the cheapest clues about a stripped Mach-O or ELF file?

    file and otool -hv (readelf -h for ELF) give the format, the processor and whether the code is position-independent; otool -Iv lists the imported library functions; strings prints its readable text.

  4. Why does a stripped file still name the library functions it calls?

    The loader binds imports by name, so those names must stay. Stripping hides the program's own names, not what it calls.

  5. How do you confirm what the imports and strings suggest before reading code?

    Run the program on inputs you choose, such as an empty word and two words one letter apart, and compare the outputs.

Key takeaways

  • Reverse engineering a binary you built means reading the compiled file to recover its behaviour, and it starts with whether you are allowed to read it at all.
  • Work from the outside in: identify the file, find its entry, then read its imports and strings, so you know the landmarks before you read any instructions.
  • A stripped file still names the library functions it imports, because the loader binds those by name, so stripping hides a program’s own names but not what it calls.
  • Running the program on inputs you choose, such as the empty word and two words one letter apart, confirms what the imports and strings suggest before any instruction is read.

Terms in this lesson

decompiler
A program that reads a binary's machine code and rebuilds C-like source from it, so that a function can be read as statements and loops rather than as instructions. Ghidra is one widely used example.
literal pool
A block of constants, such as C string literals, that the compiler keeps in the file apart from the code that uses them; instructions reach an entry through a PC-relative address.
reverse engineering
Recovering how a compiled program works from the program itself when its source is not available: its format, where it starts, what it calls and what its functions compute. Whether you may do it depends on who owns the program and on its licence.

References

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.