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.
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.
The drawing shows the whole scene; its 6 steps are listed below.
Every step as text
- 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.
- 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.
- 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.
- 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.
- Then the code. Only now read instructions, starting from the known calls: what feeds strlen, what reaches printf, and the loop between them.
- 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.
/* 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;
} 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
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.
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
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.
- Build and strip
checksum.c, then read the entry offset fromotool -land predict which section0x460falls in before you disassemble. - 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.
- Add a third string to
checksum.c, rebuild and strip, and find it withstrings; note that_mainstays gone. - Run
otool -Ivon 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.
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.
Passes can’t be bought right now, so this part can’t be opened at the moment. The free part above stands on its own. If you have a pass, enter your licence key.
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.
-
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?
-
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?
-
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?
Flashcards · 5 cards
Recall it, then check
Say the answer to yourself before you look: recalling it is what makes it stick.
-
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.
-
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.
-
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.
-
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.
-
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
- Mach-O loader.h: LC_MAIN, entry_point_command and the header flags (Apple Open Source)
- llvm-objdump, llvm-nm and llvm-strip command guides (LLVM Project)
- GNU binutils: strings (Free Software Foundation)
- The FNV non-cryptographic hash algorithm (offset basis and prime) (Internet Engineering Task Force (Eastlake and Hansen))
- Circular 92, Chapter 12: section 1201(f), reverse engineering for interoperability (U.S. Copyright Office)
- Ghidra software reverse engineering framework (National Security Agency)
Report a problem with this lesson
Kept only in this browser. Your Learn progress