Skip to content
New

DeepSeek V4.1 Flash free for 7 days: 10M tokens a day after any top-up. Until Nov 30.

How it works

Intro to Ghidra with a Benign Binary You Compile

Compile a toy C licence check two ways, then use Ghidra to find its string, follow references and compare the decompiler with your source.

Intermediate13 min readBy NotHoly

Reverse engineering is easier to learn on code you wrote. You know what the program should do, so you can check every claim the tool makes about it. This article takes that approach. You will write a small C program with a toy licence check, compile it twice, and open both builds in Ghidra. Then you will compare what the decompiler says with the source you typed.

Put the decompiler's output next to your own code and you learn quickly what the tool gets right, what it guesses, and what it loses when the compiler optimizes. That matters more than memorizing menus.

If the idea of reading machine code is new to you, read What Is Reverse Engineering? first. You do not need prior experience with assembly. You do need a terminal and about an hour.

What Ghidra is

Ghidra is a software reverse engineering framework developed by the U.S. National Security Agency and published on GitHub under the NationalSecurityAgency/ghidra repository. It runs on Windows, macOS and Linux. For a compiled program, it can do four things that matter here:

  • Disassemble the machine code into assembly instructions.
  • Decompile functions into C-like pseudocode with its built-in decompiler.
  • Find strings and cross-references (XREFs), which tell you where in the program a string or address is used.
  • Store your changes in a project, including names you give to functions and variables.

Ghidra does not get the original source back. It produces an approximation. The pseudocode is usually close in shape, but names, types and some control flow can differ from what you wrote. That gap is what this lab is designed to expose.

Why analyze a program you wrote

You could download a sample binary from a forum and start looking. Do not do that. Unknown binaries can be malware, and you will have no way to check the tool's output because you do not know the right answer.

A program you compile yourself gives you three things:

  1. Ground truth. You know that check() compares the argument with "LAB-1234".
  2. Two builds of the same code. One keeps debug information and symbols. The other is stripped and optimized. Comparing them shows what the compiler removed.
  3. A safe target. It only prints ok or no. It makes no network calls and writes no files.

Everything in this lab stays on your machine. You are analyzing your own file, which is the only scope this exercise needs.

Prepare the lab

What you need

  • A C compiler. On Linux, install gcc through your package manager. On macOS, clang from the Xcode command line tools works for this lab, and the flags are the same. On Windows, use WSL or a container with GCC.
  • A JDK for Ghidra. Ghidra runs on Java, so you need a JDK installed before the launcher will start.
  • Ghidra from the official GitHub releases.
  • A terminal with sha256sum. Git Bash on Windows includes it.

Install a JDK and Ghidra

Install the JDK that the Ghidra release asks for. Do not rely on an older guide for that number. When I checked the repository README and the Getting Started guide on the writing date, both asked for a 64-bit JDK 25, and the README linked to Eclipse Temurin builds. Read the README or release notes for the exact version you download, and use that one. A Java version mismatch is a common reason Ghidra fails to start.

Download Ghidra only from the official releases page under github.com/NationalSecurityAgency/ghidra. The repository links to the release files. Unzip the release into a new folder. The Getting Started guide says not to extract Ghidra on top of an existing installation. Start it with ghidraRun on Linux and macOS, or ghidraRun.bat on Windows.

Verify the download

Before you unzip anything, check the file against a published SHA-256 value. Run this in the folder where you saved the release:

bash
sha256sum ghidra_*.zip

Compare the printed hash with the SHA-256 value that GitHub shows next to the file on the release page, or that the project publishes. The hash must match exactly. If you cannot find a published SHA-256 for that file, write that down in your lab notes. Do not treat a hash you copied from a random mirror as a publisher's checksum.

Checking a hash only proves the file matches what the publisher listed. It does not prove the publisher is trustworthy. Use the official source and keep the file name and hash in your notes.

Write the toy program

Create an empty folder for the lab. Keep the source file and the binaries together, and keep Ghidra projects outside that folder so they do not clutter it.

bash
mkdir -p ghidra-lab && cd ghidra-lab
cat > toy.c <<'EOF'
#include <stdio.h>
#include <string.h>
int check(const char *k){ return strcmp(k, "LAB-1234") == 0; }
int main(int c, char **v){ if(c>1 && check(v[1])) puts("ok"); else puts("no"); return 0; }
EOF

The program reads one command-line argument. If the argument equals LAB-1234, it prints ok. Otherwise it prints no. There is no security value in this check. It is a fixed string in a file that anyone can read, and that is the point. You will find the string in the binary within a few minutes.

Record the hash of your source so you can prove later which file you analyzed:

bash
sha256sum toy.c

On the machine I used, this printed:

text
d9746d8fb118fa954a46b4d501ddf9b0ed86e986e7c99adb05e4a1a60ca0071d  toy.c

Your hash will differ if your file has different whitespace or line endings.

Compile it twice

Build the same source into two binaries with different settings:

bash
gcc -O0 -g -o toy_debug toy.c
gcc -O2 -s -o toy_stripped toy.c

The flags do different things. Both come from the GCC manual, which you should read once:

  • -O0 turns off most optimization. It is the default, and the GCC manual describes it as the setting that keeps debugging predictable. The code closely follows the source.
  • -O2 enables most optimizations that do not trade size for speed. The manual says it also enables inlining of small functions, even ones not declared inline. The code can look very different from the source.
  • -g adds debug information in the operating system's native format. Ghidra can read some of this, and debuggers use all of it.
  • -s removes the symbol table from the output at link time. The function names check and main are no longer stored in the file.

Two binaries with the same source and different flags is what makes the lab useful. Keep both.

Run both builds

Run them first, before you open Ghidra. A program you have not run is a program you do not understand yet.

bash
./toy_debug LAB-1234
./toy_debug wrong
./toy_debug
./toy_stripped LAB-1234
./toy_stripped wrong

I ran these inside a Debian container with GCC 14.2.0 on an x86-64 host. The output was:

text
ok
no
no
ok
no

Both builds behave the same. Stripping and optimization did not change the logic, which is what you expect. If your outputs differ, stop and find out why before you continue.

Inspect the binaries from the command line

Before Ghidra, look at what is stored in each file. Two tools are enough: nm lists symbols, and strings lists printable text.

bash
nm toy_debug | grep -E " (check|main)$"
nm toy_stripped
strings toy_stripped | grep LAB
ls -l toy_debug toy_stripped

On the machine I used, the output was:

text
0000000000001149 T check
0000000000001175 T main
nm: toy_stripped: no symbols
LAB-1234
-rwxr-xr-x 1 root root 17384 Nov  8 09:17 toy_debug
-rwxr-xr-x 1 root root 14480 Nov  8 09:17 toy_stripped

Read those lines carefully:

  • The debug build still names check and main. Those names came from your source.
  • The stripped build has no symbols. nm has nothing to list.
  • The string LAB-1234 survives in both files. A string is data, and stripping does not remove data the program needs at runtime. The string is in the file as a constant, and it is what makes this lab easy to start with.

Now disassemble check in the debug build:

bash
objdump -d -M intel toy_debug | grep -A14 "<check>:"

The output looked like this:

text
0000000000001149 <check>:
    1149:	55                   	push   rbp
    114a:	48 89 e5             	mov    rbp,rsp
    114d:	48 83 ec 10          	sub    rsp,0x10
    1151:	48 89 7d f8          	mov    QWORD PTR [rbp-0x8],rdi
    1155:	48 8b 45 f8          	mov    rax,QWORD PTR [rbp-0x8]
    1159:	48 8d 15 a4 0e 00 00 	lea    rdx,[rip+0xea4]        # 2004 <_IO_stdin_used+0x4>
    1160:	48 89 d6             	mov    rsi,rdx
    1163:	48 89 c7             	mov    rdi,rax
    1166:	e8 d5 fe ff ff       	call   1040 <strcmp@plt>
    116b:	85 c0                	test   eax,eax
    116d:	0f 94 c0             	sete   al
    1170:	0f b6 c0             	movzx  eax,al
    1173:	c9                   	leave
    1174:	c3                   	ret

You can see the source in these instructions. A call to strcmp, a test of the return value, and a sete that turns "equal to zero" into a 0 or 1. Keep this in mind when you read the decompiler.

Now look at the optimized build. I disassembled toy_stripped the same way. The compiler put the comparison straight into main, which sits at 0x1060 in my build: a cmp edi,0x1 for the c>1 test, then mov rdi,[rsi+0x8] to load v[1], then call strcmp@plt. It still kept a separate out-of-line copy of check at 0x1190, because the function is not static and another file could call it. Addresses will differ with your toolchain, so treat these as a map of what to look for and not as numbers to match.

Import into Ghidra

The exact menu labels depend on the Ghidra version, and I did not run the Ghidra GUI on the machine where this article was checked. The steps below follow the official documentation and what the interface usually looks like. Check each name against your version. Take a screenshot of your own screen when you finish each step, and do not reuse screenshots from a tutorial.

  1. Start Ghidra with ghidraRun (or ghidraRun.bat).
  2. Choose File > New Project. Pick Non-Shared Project and give it a name, such as toy-lab. Store it in a folder you control.
  3. Choose File > Import File. Select toy_debug. Accept the detected format if it matches your system. A file built on x86-64 Linux should show as ELF with the x86 processor.
  4. Repeat the import for toy_stripped. You now have two entries in the project.
  5. Open each file in the CodeBrowser tool. When Ghidra asks whether to analyze the program, accept the default analysis for this lab.

Wait for the analysis to finish. Ghidra shows progress in the bottom bar. Do not start renaming anything until analysis is done.

Find the string and follow its references

Start with toy_stripped, because that is the harder case. Then compare with toy_debug.

  1. Open Window > Defined Strings.
  2. Find LAB-1234 in the list. Double-click it to jump to its address in the listing.
  3. Right-click the string's address and open the references menu. Look for an item like References > Show References to. Each reference is a place in code that reads this address.
  4. Follow the reference. It leads you into the function that compares your input with the string.

In the debug build, Ghidra should also show the function name check from the symbol table. In the stripped build, it will show a generated name such as FUN_00101190. The number depends on the build, so use the name Ghidra shows on your screen.

Do the same with the "ok" and "no" strings. Each is used in main. Following them from main gives you the same picture from the other side.

Read the decompiler next to your source

Open the decompiler pane for the function that compares the string. Put toy.c beside it in another window.

Here is the kind of result to look for, written as an illustration of how to read it and not as output from a specific Ghidra version:

  • A function with one argument that is a pointer to characters.
  • A call to a function named strcmp or a symbol that the tool labels as strcmp.
  • A comparison that returns a 1 when the result is zero.

Now list the differences you see. Typical ones are:

  • Types. The decompiler may show a char * as char *, or use a looser type such as undefined8. It may not know the input is const.
  • Names. Your parameter k will appear as param_1 or a similar placeholder.
  • Return value. The C expression strcmp(...) == 0 may appear as a comparison followed by a conditional return. The meaning is the same, but the shape is not.
  • Argument handling in main. Your c > 1 check becomes a comparison on an argument count. Pay attention to the off-by-one: argv[0] is the program name, and v[1] is the first real argument.

Do not grade the decompiler. Notice where its story and yours match, and where they split. When they split, go back to the disassembly and the source and decide which one is right.

Rename variables in the stripped build

Now do the part that makes a stripped binary readable.

  1. In the decompiler pane, click the parameter that holds the input string.
  2. Right-click and choose Rename Variable, or select the variable and press L. Name it input.
  3. Rename the function itself in the listing or the symbol tree to check_input.
  4. In main, rename the argument array variable to argv and the count to argc.

Ghidra stores these names in your project. The binary on disk does not change. That matters: you are documenting your reading, not editing the file you compiled.

Notice how much easier the code becomes after renaming. At first the variable names were guesses. Now they match your source. That is the process reverse engineers use every day, and it is also why names in a decompiled program should be treated as hypotheses until you have checked them.

Compare the two builds

Put the debug decompilation and the stripped decompilation side by side. Answer these questions in your notes:

  • Which function names survived in the stripped build?
  • Did the -O2 build inline check into main, or keep a separate function? Look in the listing for a separate strcmp call site and a separate ret.
  • Which parts of your source are easy to see in both views?
  • Which parts are hard to find in the stripped view?

Write one sentence per answer. If you cannot answer one, that is also data. Go back to the listing and find the instruction that makes it confusing.

Common mistakes

Skipping the run step. If you open the binary in Ghidra before you run it, you do not know what the correct behavior is. Run it first and record the outputs.

Trusting the decompiler's names. A name like FUN_00101190 or local_18 is a placeholder. Renaming it is your claim, and it needs evidence.

Mixing up builds. The debug and stripped binaries have different addresses and different names. Keep the file names in your notes, and check which project entry you are in before you rename anything.

Using a sample from an untrusted source. Your own compiled file is the safe starting point. Once you are comfortable here, the next step is still your own code or a sample from a source you have checked, and you should verify the source first. The article on file hashes and VirusTotal ethics explains why a hash alone does not settle that question.

Treating the lab as a security test. The string check here is trivially broken. Finding it proves nothing about real software. Real licence and protection code is harder, and analyzing it has to stay within the scope you are allowed to work in.

Forgetting the JDK version. If Ghidra will not start, check the Java version against the release notes before you touch anything else.

How to go further

Once the toy works, make the exercise harder in controlled ways:

  • Change "LAB-1234" to a longer string and recompile. Find it again in both builds.
  • Add a second check function that compares the input with a number, then find where Ghidra shows the conversion.
  • Build with -O0 -s and compare that with -O2 -s. The difference shows how much the optimizer changes code shape without removing the logic.
  • Read Ghidra's own documentation for the decompiler and the listing view. The official site and the GitHub docs folder cover the interface in more depth than this article can.

If you want to understand the compiler flags better, the GCC manual sections on optimization and debugging options are the primary sources. Read them before you rely on a flag in a report.

Keep your notes. A good lab notebook has the source hash, the compile commands, the run outputs, the Ghidra version, the JDK version and the names you assigned, each with the evidence that supports it. That record turns this exercise into a skill you can reuse on the next program.

Sources

Sources

  1. Ghidra: Getting Started (GitHub repository)
  2. Ghidra repository README
  3. Ghidra project site
  4. GCC manual: Optimize Options
  5. GCC manual: Debugging Options

More in Reverse engineering