Ghidra turns a compiled binary with no source and no symbols back into readable C on your own Linux box. To install Ghidra on Ubuntu you need one thing the zip does not ship: a JDK 25 runtime, because the newest upstream release refuses to start on JDK 21. Kali’s own package pulls in OpenJDK 25 for you.
This guide covers the upstream zip on Ubuntu 26.04 and 24.04, the apt package on Kali, and a first analysis of a stripped binary. We checked both paths in October 2026 on Ubuntu 26.04 LTS and Kali Rolling with Ghidra 12.2.
Which JDK does Ghidra need?
The current Ghidra release needs JDK 25 or newer, and it has to be a full JDK, because ghidraRun calls its launcher in jdk mode and will not accept a bare JRE. The project’s release notes say it plainly (“requires, at minimum, JDK 25 to run”), and the previous 12.1 line was the last one that ran on JDK 21. If you followed an older tutorial that installed openjdk-21-jdk, the launcher will stop and ask you for a JDK path instead of opening.
What changes between the three systems we tested is where Ghidra and that JDK come from, and how far behind upstream each source runs. The timings in the last row are the full headless import, analysis and decompile of the test binary used later in this guide.
| Item | Ubuntu 26.04 | Ubuntu 24.04 | Kali Rolling |
|---|---|---|---|
| Ghidra source | Upstream zip | Upstream zip | apt install ghidra or upstream zip |
| Ghidra version tested | 12.2 | 12.2 | 12.1.4 (apt), 12.2 (zip) |
| JDK package | openjdk-25-jdk 25.0.4.1 | openjdk-25-jdk 25.0.4.1 | openjdk-25-jdk 25.0.5-ea (pulled in by apt) |
| Native distro package | None | None | ghidra 12.1.4+ds-0kali1 |
| Snap | Unofficial, 12.1.2 | Unofficial, 12.1.2 | Not used |
| Headless decompile of the test binary | 10.2 s | 12.4 s | 6.4 s (apt build) |
Ubuntu has no Ghidra package in its own archive, and the only snap (snap find ghidra) is an unofficial one that was still on 12.1.2 when we looked, three releases behind upstream. We would skip it and use the zip, which comes from the NSA’s GitHub releases page with a published SHA-256. The Kali package is the easiest route on Kali, with one bug in its Help viewer that we hit on every launch and cover below.
What do you need before you start?
Ghidra is a desktop Java application, so the machine needs a graphical session for the GUI part of this guide. Headless analysis works over SSH with no display at all. Check these before you install:
- Your system runs Ubuntu 26.04, Ubuntu 24.04 or Kali Rolling on x86_64.
- Your user can run
sudo. - At least 4 GB of RAM is free (the GUI used 530 MB with one small binary open).
- About 2 GB of disk is free for the zip, the extracted 885 MB tree and the JDK.
- The machine can reach github.com to download the release.
Our lab VMs had 4 cores and 6 GB of RAM, which is a floor for small binaries. Real malware samples or large firmware images need more heap, and Ghidra’s default maximum heap is typically a quarter of system RAM, so 16 GB is a better target for daily work. You can raise the ceiling with MAXMEM_DEFAULT in ghidraRun or the GHIDRA_MAXMEM environment variable.
How do you install Ghidra on Ubuntu 26.04 and 24.04?
You install OpenJDK 25 from the Ubuntu archive, download the latest zip from GitHub, check its hash and unpack it under /opt. Every command in this section ran unchanged on both Ubuntu 26.04 and 24.04, because both releases carry openjdk-25-jdk in their archive (main on 26.04, universe on 24.04).
Install OpenJDK 25
Install the full JDK, not openjdk-25-jre, together with the tools the download step uses. The launcher checks for a JDK specifically (support/launch.sh runs in jdk mode for the GUI), so a JRE on its own is not enough.
sudo apt update
sudo apt install -y openjdk-25-jdk unzip wget curl
Confirm that the default java is now version 25. On a machine that already had another JDK, the default comes from update-alternatives, and sudo update-alternatives --config java switches it. Ghidra’s launcher does its own search, but your shell, your IDE and any scripts you write will use what java points at.
java -version
On Ubuntu 26.04 the build string ends in 26.04.4-Ubuntu, and on 24.04 it reads 24.04.4-Ubuntu with the same version number. Both releases get the same OpenJDK 25 update stream, so you will not find a version gap between the two LTS releases for Ghidra’s purposes.
openjdk version "25.0.4.1" 2026-08-18
OpenJDK Runtime Environment (build 25.0.4.1+1-1-26.04.4-Ubuntu)
OpenJDK 64-Bit Server VM (build 25.0.4.1+1-1-26.04.4-Ubuntu, mixed mode, sharing)
You do not have to remove JDK 21 if another tool needs it. Ghidra’s launcher checks JAVA_HOME_OVERRIDE, then JAVA_HOME, then the java on your PATH, then the JDK it used last time, and finally scans /usr/lib/jvm, skipping anything older than its minimum. In our test it ended up on JDK 25 even with JAVA_HOME pointing at JDK 21, because the java on PATH was 25.
Download the latest Ghidra release
The zip name carries both the version and a build date, so rather than copy a link that goes stale, ask the GitHub API for the latest asset URL:
GHIDRA_URL=$(curl -s https://api.github.com/repos/NationalSecurityAgency/ghidra/releases/latest | grep browser_download_url | cut -d '"' -f 4)
echo "$GHIDRA_URL"
A single URL to the PUBLIC zip should print. If the line comes back empty, your IP hit GitHub’s limit of 60 unauthenticated API calls per hour, so wait for the hourly reset or copy the link from the releases page:
https://github.com/NationalSecurityAgency/ghidra/releases/download/Ghidra_12.2_build/ghidra_12.2_PUBLIC_20261016.zip
Download it and print its SHA-256. The file is about 544 MB and took 35 seconds on our lab link, so on a slow connection this is the step to leave running while you read ahead.
cd ~
wget "$GHIDRA_URL"
sha256sum ghidra_*.zip
Compare the hash with the one in the release notes on GitHub, because the zip is the only thing standing between you and a tampered reverse engineering tool on your machine. Ours matched the published value.
2c2cd776db3c1f6a097ba418922378ac3649cadd5e844a6f5f088ae628a7be67 ghidra_12.2_PUBLIC_20261016.zip
A mismatch means a broken or altered download. Delete the file and fetch it again rather than unpacking it.
Extract Ghidra to /opt
Unpack the zip under /opt and point a stable /opt/ghidra symlink at the versioned directory. The symlink is what you will put in launchers and scripts, so the next upgrade is one unzip plus one ln. Keeping the old directory around until the new release has opened your projects also gives you an instant rollback, because switching back is the same ln command.
sudo unzip -q ghidra_*_PUBLIC_*.zip -d /opt
sudo ln -sfn /opt/$(ls /opt | grep ^ghidra_ | sort -V | tail -1) /opt/ghidra
ls -l /opt/ghidra
The link should resolve to the release you just unpacked. If it points at an older directory, an earlier install is still sitting in /opt, and the sort -V pipeline picked the highest version, so check the directory names before you go on.
lrwxrwxrwx 1 root root 23 Oct 23 12:46 /opt/ghidra -> /opt/ghidra_12.2_PUBLIC
The screenshot shows the whole sequence on our Ubuntu 26.04 VM, from the JDK check to the version recorded in Ghidra’s own application.properties. That last file is the reliable way to tell which release a directory holds, because the folder name can be renamed.

The extracted tree is 885 MB. Nothing is written outside /opt until the first launch, when Ghidra creates its settings under ~/.config/ghidra/ and a cache under /var/tmp/<user>-ghidra.
Add a desktop launcher
The zip has no .desktop file, so Ghidra will not show up in the GNOME app grid until you add one. Create it in your home directory, which needs no sudo:
mkdir -p ~/.local/share/applications
vim ~/.local/share/applications/ghidra.desktop
Add the following entry. StartupWMClass matches the window class Ghidra really sets (ghidra-Ghidra, read with xprop in our lab), which keeps the running window grouped under this icon in the dock:
[Desktop Entry]
Type=Application
Name=Ghidra
Comment=NSA software reverse engineering suite
Exec=/opt/ghidra/ghidraRun
Icon=/opt/ghidra/support/ghidra.ico
Terminal=false
Categories=Development;Debugger;
StartupWMClass=ghidra-Ghidra
Validate the file, which prints nothing when the entry is clean. Our first draft listed Security as a category. The validator printed a hint suggesting Settings or System alongside it, so we switched to Debugger. The tool comes from the desktop-file-utils package if your system lacks it.
desktop-file-validate ~/.local/share/applications/ghidra.desktop
From a terminal, /opt/ghidra/ghidraRun starts the same thing. The status bar reported 52 seconds for the first start, because the clock keeps running while the user agreement waits for you to click, and every launch after that finished in about 4 seconds.
How do you install Ghidra on Kali Linux?
On Kali, sudo apt install ghidra is all it takes, because the package depends on openjdk-25-jdk and installs it for you. The trade-off is that Kali builds Ghidra from source and trails upstream, so on the day we tested it gave us 12.1.4 while GitHub already had 12.2. Install it from the Kali repositories:
sudo apt update
sudo apt install -y ghidra
Budget for the download size. apt fetched 1,002 MB, because the package also pulls ghidra-data (Function ID databases) and the full JDK, and it took just under two minutes:
Need to get 1,002 MB of archives.
After this operation, 1,315 MB of additional disk space will be used.
Check what you got and which JDK came with it. The version string tells you how far the package trails GitHub, and the Depends line confirms the JDK requirement holds on Kali too.
apt-cache show ghidra | grep -E "^(Version|Depends|Recommends|Installed-Size)"
java -version
Kali ships an early-access build of OpenJDK 25 (25.0.5-ea), which ran Ghidra fine in our tests. Ubuntu’s build is a GA release, so if you see odd JVM crashes on Kali, the early-access JDK is the first thing to suspect. The package metadata and the JDK it pulled in look like this on our Kali VM.

The package puts a ghidra command in /usr/bin, a symlink to /usr/share/ghidra/ghidraRun, so you start it by typing ghidra. The About dialog confirms it is a Kali build and not the NSA one, because it says Build DEV instead of Build PUBLIC:

That DEV build is also why its settings live in ~/.config/ghidra/ghidra_12.1.4_DEV and not in a _PUBLIC folder. If you later switch to the upstream zip, Ghidra starts with fresh preferences, although your projects open fine in the newer release.
When should you use the upstream zip on Kali?
Use the zip on Kali when you need the newest release or a working Help browser. The packaged build threw IllegalArgumentException - view is invalid on first launch and again every time we opened Help, while the upstream zip on the same VM opened Help with no error.
Since apt has already installed JDK 25, the Ubuntu download and extract steps above work on Kali unchanged, and both builds can sit side by side: ghidra runs the package, /opt/ghidra/ghidraRun runs the zip.
For a Kali VM you use for coursework or CTFs, next to tools like Wireshark for network analysis, the package is fine. If you are building a dedicated pentest lab on Proxmox with Kali, we would put the zip in the template so every clone runs the same upstream release.
What happens the first time Ghidra starts?
Ghidra stacks three windows on top of the project manager the first time, and only one of them needs an answer. On our Ubuntu VM the first start looked like this:
- The Ghidra User Agreement appears, and clicking I Agree stores the answer so it does not come back.
- The Help browser opens on the What’s New page for the release you installed.
- A Tip of the Day dialog sits over the project window until you close it.
- The project window title reads
Ghidra: NO ACTIVE PROJECTonce the dialogs are gone.
Close the Help and the tip, and you land on the project manager, which is where every analysis starts. Before going further, open Help, About Ghidra, because it is the quickest way to confirm which release and which Java the launcher picked up.

If Java Version shows anything older than 25, the launcher found a different JDK from the one you expected, and the troubleshooting section at the end shows how to pin it.
How do you analyze your first binary in Ghidra?
You create a project, import the binary, let auto-analysis run, then work from a string you recognise back to the code that uses it. We built a small licence checker and stripped it, so there are no function names to lean on. That is what most real targets look like, and it is the exercise that shows what the decompiler is for.
Build a stripped test binary
Install a compiler if you do not have one, and make a working folder. Kali ships gcc already, while a fresh Ubuntu desktop does not, so on Ubuntu this line is the one that matters.
sudo apt install -y gcc
mkdir -p ~/re-lab
vim ~/re-lab/licence.c
Paste this program. The key is stored XOR’d with 0x42, so it never appears in the binary as readable text and strings alone cannot hand it to you. That is the whole point of the exercise.
#include <stdio.h>
#include <string.h>
static const unsigned char key[] = {0x31, 0x3a, 0x2d, 0x3b, 0x76, 0x2b, 0x77, 0x75, 0x2b, 0x77, 0x76};
static int check(const char *input)
{
size_t n = sizeof(key);
if (strlen(input) != n)
return 0;
for (size_t i = 0; i < n; i++) {
if ((unsigned char)(input[i] ^ 0x42) != key[i])
return 0;
}
return 1;
}
int main(int argc, char **argv)
{
if (argc != 2) {
printf("usage: %s <licence-key>\n", argv[0]);
return 2;
}
if (check(argv[1])) {
puts("licence accepted");
return 0;
}
puts("invalid licence");
return 1;
}
Compile it with -s to strip the symbol table, then try a wrong key. Stripping matters here, because without it Ghidra would simply show you a function called check and there would be nothing to find.
cd ~/re-lab
gcc -O0 -s -o licence licence.c
file licence
./licence hunter2
file reports the binary as stripped, and the program rejects the guess. From here on, pretend you never saw the source, because that is the position you are in with any binary someone sends you.
licence: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, BuildID[sha1]=9f4803f3bd224ee170c53402cd435fa55ba35405, for GNU/Linux 3.2.0, stripped
invalid licence
Before opening Ghidra, see how far strings gets you. This is the step Ghidra replaces, so it is worth seeing exactly where it runs out.
strings -n 6 licence | head -20
The messages are visible, and so is the encoded key, glued to the front of the usage string as 1:-;v+wu+wv. Nothing in this output tells you that it is the key, how long it is, or how it is checked:
/lib64/ld-linux-x86-64.so.2
strlen
__libc_start_main
__cxa_finalize
printf
libc.so.6
GLIBC_2.2.5
GLIBC_2.34
_ITM_deregisterTMCloneTable
__gmon_start__
_ITM_registerTMCloneTable
1:-;v+wu+wvusage: %s <licence-key>
licence accepted
invalid licence
GCC: (Ubuntu 15.2.0-16ubuntu1) 15.2.0
.shstrtab
.note.gnu.build-id
.interp
.gnu.hash
.dynsym
You could guess that the odd string is the key, but you would still be guessing at the transform. Ghidra answers that from the code itself.
Create a project and import the binary
Ghidra keeps all analysis in a project, so open File, New Project, choose Non-Shared Project, and give it a folder and a name. A shared project needs a Ghidra Server and is only worth it for a team:

Press I (File, Import File) and pick ~/re-lab/licence. Ghidra reads the ELF header and fills in the format and the language, here x86:LE:64:default:gcc, so leave both as detected and click OK. You only override the language for raw firmware dumps with no header, where Ghidra has nothing to read the architecture from and you have to tell it.

The import summary that follows lists 15 functions and only 16 instructions, because nothing has been disassembled yet. Those numbers jump once analysis runs, which is a quick way to tell an analyzed program from one that was only imported. A line saying [libc.so.6] -> not found in project is normal: Ghidra is telling you it will not follow calls into libc, which you do not need here.
Run auto-analysis
Double-click licence in the project tree to open it in the CodeBrowser, and answer Yes when Ghidra asks to analyze it. Keep the default analyzers for a first pass; the ones marked Prototype in red are experimental and off by default for a reason:

Analysis of a binary this size takes about 2 seconds. A large firmware image or a statically linked binary takes far longer, and while it runs the progress bar sits in the bottom right corner of the CodeBrowser.
Find the check from a string
With no names to search for, start from the one thing you know: the program prints licence accepted. Open Window, Defined Strings and type licence into the filter at the bottom:

Double-click the licence accepted row and the Listing jumps to it in .rodata. Next to the string is an XREF to FUN_00101209, the code that uses it. Double-click that reference, and the Decompiler pane on the right shows the function as C:

That is main, even though stripping removed its name. It checks for two arguments, passes the second one to FUN_00101189, and prints the success message only when that call returns non-zero. So FUN_00101189 is the check, and it is the only function you need to read.
Rename functions as you go
Double-click FUN_00101189 in the decompiler to open it, then click its name and press L to rename it. We called it check_licence and renamed param_1 to input the same way. Ghidra rewrites every reference, and the decompiler becomes easier to read:

Now the logic is plain: the input must be 0xb (11) bytes long, and each byte XOR’d with 0x42 must equal the matching byte of the stored data. The decompiler shows that data as the start of "1:-;v+wu+wvusage: ...", because the 11-byte key array has no terminating zero and sits directly in front of the usage string in memory. That is a common decompiler artefact: only the first 11 bytes matter, which the loop bound tells you.
XOR is its own inverse, so applying 0x42 to those 11 bytes gives the key back. A one-line Python call is enough, and you do not need to run the binary under a debugger at all.
python3 -c 'print("".join(chr(ord(c) ^ 0x42) for c in "1:-;v+wu+wv"))'
Python prints the plain key. Notice that nothing about it was guessed: the length, the constant and the stored bytes all came from the decompiler.
sxoy4i57i54
Run the program with the recovered key to close the loop. Only do this with binaries you built or trust, because testing a key means executing the target.
./licence sxoy4i57i54
It prints licence accepted. Press Ctrl+S to save, so the renames are kept in the project for next time. The addresses you see may differ from ours, because a different GCC lays the code out differently: on Kali, GCC 16 put the check at FUN_00101159, while Ubuntu’s GCC 15 and GCC 13 both produced FUN_00101189. The decompiled logic was identical on all three.
Can Ghidra run without the GUI?
Yes. support/analyzeHeadless imports, analyzes and runs scripts with no display, which is how you batch-process samples on a server or in CI. We used it to prove the install on a plain SSH session before ever opening the GUI. Make folders for scripts and projects, then create a small script:
mkdir -p ~/ghidra-scripts ~/ghidra-projects
vim ~/ghidra-scripts/DumpDecomp.java
This script prints the decompiled C for every function in the program. Ghidra compiles it on the fly when it runs, so there is no build step and no jar to manage.
// Print decompiled C for every non-thunk, non-external function.
// @category Examples
import ghidra.app.decompiler.DecompInterface;
import ghidra.app.decompiler.DecompileResults;
import ghidra.app.script.GhidraScript;
import ghidra.program.model.listing.Function;
public class DumpDecomp extends GhidraScript {
@Override
protected void run() throws Exception {
DecompInterface ifc = new DecompInterface();
ifc.openProgram(currentProgram);
for (Function f : currentProgram.getFunctionManager().getFunctions(true)) {
if (f.isThunk() || f.isExternal()) {
continue;
}
DecompileResults res = ifc.decompileFunction(f, 60, monitor);
if (res.decompileCompleted()) {
println("==== " + f.getName() + " @ " + f.getEntryPoint());
println(res.getDecompiledFunction().getC());
}
}
ifc.dispose();
}
}
The headless options you will use most are few, and each one maps to a step you just did in the GUI. Everything else in analyzeHeadless is a variation on these.
-import <file>adds the binary to the project and runs auto-analysis on it.-scriptPath <dir>tells Ghidra where your own scripts live.-postScript <name>runs a script after analysis finishes.-overwritereplaces a program that is already stored in the project.
Run a fresh import with the script as a post-analysis step. We gave it a new project name, licence-headless, so it does not collide with the project the GUI created:
/opt/ghidra/support/analyzeHeadless ~/ghidra-projects licence-headless -import ~/re-lab/licence -scriptPath ~/ghidra-scripts -postScript DumpDecomp.java
The run took about 10 seconds wall clock on Ubuntu 26.04 and 12.4 on 24.04, with the analysis itself at 2 seconds and the rest spent starting the JVM. The report lines at the end tell you whether each stage worked:

Further up, the script printed the same check function that the GUI showed, before any renaming. Because the output is plain text, you can grep a whole folder of samples for an XOR constant or a suspicious call without opening a single window.
undefined8 FUN_00101189(char *param_1)
{
size_t sVar1;
undefined8 uVar2;
ulong local_18;
sVar1 = strlen(param_1);
if (sVar1 == 0xb) {
for (local_18 = 0; local_18 < 0xb; local_18 = local_18 + 1) {
if ((byte)(param_1[local_18] ^ 0x42U) != "1:-;v+wu+wvusage: %s <licence-key>\n"[local_18]) {
return 0;
}
}
uVar2 = 1;
}
else {
uVar2 = 0;
}
return uVar2;
}
On Kali’s package the script path is /usr/share/ghidra/support/analyzeHeadless. The run there finished in 6.4 seconds with the same result. Running the same -import twice into one project fails with ERROR REPORT: Found conflicting program file in project: /licence; add -overwrite to replace the stored program, or use -process to rerun scripts on what is already there.
Why does Ghidra say “JDK 25+ could not be found”?
Ghidra prints that when no JDK of version 25 or newer is installed, which is what happens when an older guide had you install JDK 21. We reproduced it by removing JDK 25 from the Ubuntu VM and launching again:

Do not type a JDK 21 path into that prompt, because Ghidra checks the version and will not accept it. The fix takes three steps:
- Press
Ctrl+Cto leave the prompt. - Install the JDK with
sudo apt install -y openjdk-25-jdk. - Start Ghidra again; the launcher finds JDK 25 in
/usr/lib/jvmwithout asking.
If several JDK 25 builds are installed and you want a specific one, set JAVA_HOME_OVERRIDE in /opt/ghidra/support/launch.properties to that JDK’s home directory, such as /usr/lib/jvm/java-25-openjdk-amd64. That pins the choice for every launch, GUI and headless alike, until you replace the Ghidra directory on the next upgrade.
Two caveats from the Getting Started notes: if the override points at an incompatible Java, Ghidra quietly falls back to auto-detection, and some java must still be on PATH or in JAVA_HOME for the launcher to start at all.
Error: “Unable to prompt user for JDK path, no TTY detected”
This is the same missing-JDK problem. It shows up when a desktop launcher, an SSH command or a script starts Ghidra with no terminal for the prompt. Running analyzeHeadless over plain SSH printed it on our VM, along with a pointer to the Getting Started troubleshooting notes. The fix is identical: install JDK 25.
Error: “IllegalArgumentException – view is invalid” on Kali
Kali’s packaged Ghidra showed this right after we accepted the user agreement on first launch, and again whenever we chose Help, Contents. It appears in a modal dialog, so the first time it looks like the install is broken.

The stack trace in ~/.config/ghidra/ghidra_12.1.4_DEV/application.log starts in javax.help.search.MergingSearchEngine.merge, which means the Help viewer is failing to load its search index; analysis is not affected. Click OK and carry on, or install the upstream zip next to the package. The upstream build on the same Kali VM opened Help with no error:

The error is cosmetic for analysis work. The packaged build still imported, analyzed and decompiled the test binary in our headless run, so you only need the zip if you rely on the built-in Help.
Ghidra setup checklist
Run through these on your own machine before you start on real samples. Each item is a check you can do in under a minute, and together they cover every failure we hit in the lab.
java -versionreports version 25 or newer.- The SHA-256 of the zip matches the value in the GitHub release notes.
/opt/ghidrapoints at the release you unpacked, orghidraruns the Kali package.- Help, About Ghidra shows a Java version of 25 or newer.
- A test binary imports, analyzes and decompiles in the CodeBrowser.
analyzeHeadlessends withREPORT: Import succeeded.- Unknown samples are analyzed in a VM you can throw away, not on your daily desktop.
The last item matters more than it looks. Ghidra itself never runs the binary. The moment you test a recovered key or attach the debugger, though, you are executing someone else’s code, so keep that work on a lab VM like the ones in our Kali install guide. If another tool on the same machine still needs Java 21, our OpenJDK guide for Ubuntu 26.04 covers running several JDKs side by side, and the Kali Java guide does the same on Kali.
To upgrade Ghidra later, delete the old zip from your home directory first, because the sha256sum and unzip lines use a wildcard and would pick up both archives. Then download the next zip, unpack it under /opt and rerun the ln -sfn line. Your projects open in the new release, although files it saves may no longer open in the old one, so keep a backup of anything you still need in the older build.