Binwalk
Binwalk scans a file for known magic-byte signatures and tells you what is packed inside it: compressed blobs, filesystems, bootloaders, certificates, and appended files. Reach for it the moment you are handed a firmware dump or a suspicious binary and need a map before you carve, because almost every next step depends on knowing what the file contains and where each piece begins.
Mental model: binwalk [action] [options] TARGET_FILE. The default action is a signature scan; -e extracts, -E graphs entropy, everything else is one flag away.
New to firmware? Core concepts
- Signature (magic bytes): a byte pattern that names a known format; the default
binwalk TARGET_FILEis a signature scan - Offset: where a detected item starts, shown in
DECIMALandHEXADECIMAL; you carve by offset later - Extraction (
-e): pulls detected items into_TARGET_FILE.extracted/using matching unpackers - Recursive / Matryoshka (
-M): keeps unpacking what it just extracted, firmware blob down to filesystem - Entropy (
-E): how random the bytes are; flat-high means compression or encryption, flat-low means padding - Carving: copying a byte range out into its own file (
ddby hand, or-Dby rule) - Filesystem: SquashFS, JFFS2, cramfs, UBI, the root filesystem and usually the prize in firmware
- Unpacker: the external helper (
unsquashfs,mtd-utils) Binwalk shells out to so-ecan actually extract
When to reach for Binwalk (and when not)
Reach for it to map an unknown firmware image, find data appended or embedded in a CTF artifact, confirm what a packed or self-extracting binary contains, or unpack your own router image to read its filesystem and configs. It surfaces structure so you can carve it out; it does not exploit anything.
Reach for something else when you already know the format (use unzip, tar, unsquashfs directly), when the hidden data is image or audio steganography (steghide, zsteg), when a region is encrypted rather than compressed (Binwalk finds it but cannot decrypt it), or when it stalls on a deeply nested container and you want a more aggressive recursive unpacker (unblob).
Install, update, verify#
sudo apt install -y binwalk # Debian / Ubuntu / Kali
sudo dnf install -y binwalk # Fedora / RHEL
pipx install binwalk # pip route (older 2.x)
# Extraction helpers, so -e can actually unpack what it detects
sudo apt install -y mtd-utils squashfs-tools p7zip-full \
zstd lzop unrar cabextract sleuthkit
sudo apt install --only-upgrade -y binwalk # update
binwalk --help # version sits in the banner (2.x); 3.x: binwalk --version
command -v binwalk # confirm it is on PATH
Self-test without touching anything external: printf 'hello ' > t; echo data | gzip >> t; binwalk t should print a DECIMAL HEXADECIMAL DESCRIPTION table with a gzip compressed data row. If a filesystem detects but never extracts later, you are missing one of the helper unpackers above.
Flags you’ll actually use#
Skim these first; the cheat sheet below is these flags combined.
| Flag | Does | Flag | Does |
|---|---|---|---|
-e | Extract known types to _file.extracted/ | -y | Only show/extract matching signatures |
-M | Recurse into extracted files (Matryoshka) | -x | Exclude matching signatures |
-d N | Cap -M recursion depth | -A | Scan for executable opcode signatures |
-C DIR | Extract into DIR, not beside the file | -Y | Identify CPU arch (capstone disasm) |
-r | Delete carved files after extraction | -E | Entropy graph (compression vs encryption) |
-z | Carve regions, skip external extractors | -J | Save the entropy plot as a PNG |
-D / --dd | Carve type:ext[:cmd] yourself | -o / -l | Start offset / bytes to scan |
-R | Scan for a raw byte sequence | -t | Fit the table to the terminal width |
-f / -c | Log to a file / as CSV | -v / -q | Verbose / quiet |
-B | Signature scan (the default) | -h | Help and version banner |
Cheat sheet#
Every block starts simple and adds one flag at a time, so you can stop at the line that does the job.
# Build up a scan: stop at the line you need
binwalk TARGET_FILE # 1. signature map: what is inside
binwalk -t TARGET_FILE # 2. + fit the table to your terminal
binwalk -y filesystem TARGET_FILE # 3. + only filesystem rows
binwalk -x lzma -x zlib TARGET_FILE # 4. or drop noisy false-positive classes
# Extract
binwalk -e TARGET_FILE # extract into _TARGET_FILE.extracted/
binwalk -e -C extracted TARGET_FILE # extract into a tidy directory
binwalk -Me TARGET_FILE # recursive: unpack firmware layers
binwalk -Me -d 4 TARGET_FILE # cap recursion depth at 4 levels
binwalk -re TARGET_FILE # extract, then delete the carved blobs
# Carve by hand or by rule
binwalk TARGET_FILE | grep -i squashfs # find the part, note the DECIMAL offset
dd if=TARGET_FILE of=fs.bin bs=1 skip=OFFSET 2>/dev/null # carve from that offset
binwalk -D 'squashfs:squashfs' TARGET_FILE # rule-based carve to a .squashfs file
binwalk -z TARGET_FILE # carve regions, but do not run extractors
# Analyze without extracting
binwalk -E TARGET_FILE # entropy graph (needs a display)
binwalk -E -J TARGET_FILE # entropy, saved as a PNG
binwalk -A TARGET_FILE # find executable opcodes in a raw blob
binwalk -Y TARGET_FILE # guess CPU architecture (ARM/MIPS/...)
# Scope the scan to part of a big file
binwalk -o 1048576 TARGET_FILE # start scanning at byte 1048576
binwalk -o 1048576 -l 65536 TARGET_FILE # scan a 64 KB window from that offset
binwalk -R '\x1f\x8b\x08' TARGET_FILE # hunt a raw byte sequence (gzip magic)
# Record what you found
binwalk -f scan.log TARGET_FILE # log the table to a file
binwalk -f scan.csv -c TARGET_FILE # log the table as CSV (diffable)
binwalk TARGET_FILE | tee notes/scan.txt # save the human-readable table
sha256sum TARGET_FILE | tee notes/hash.txt # hash the original before you touch it
Command breakdowns#
Each block below explains one situation, the exact command, and what to expect back.
Map an unknown file with a signature scan#
binwalk TARGET_FILE
Prints a DECIMAL HEXADECIMAL DESCRIPTION table, one row per recognized signature. This is your map of the file: every later decision reads off these offsets and format names.
Extract everything Binwalk recognizes#
binwalk -e TARGET_FILE
Unpacks each detected item into _TARGET_FILE.extracted/ using matching helpers. Inspect it with ls -R _*.extracted. If a format detects but no folder appears, the unpacker for it is not installed.
Unpack a firmware image layer by layer#
binwalk -Me TARGET_FILE
-M (Matryoshka) re-runs extraction on everything it just pulled out, so a firmware blob unwraps through its compression layers down to the root filesystem. This is the standard firmware command.
Keep recursion from exploding on a huge image#
binwalk -Me -d 4 -C extracted TARGET_FILE
-d 4 caps recursion at four levels and -C sends output to extracted/ instead of beside the file. Use both when -Me on a large image threatens to fill the disk with nested folders.
Extract into a tidy directory#
binwalk -e -C extracted TARGET_FILE
-C keeps every _*.extracted tree under one directory you chose, so output never clobbers your workspace or buries the original next to a pile of carved blobs.
Show only the filesystem rows#
binwalk -y filesystem TARGET_FILE
-y filters the scan to signatures whose description matches filesystem, so SquashFS, JFFS2, cramfs, and UBI rows surface without the gzip and lzma noise. The fast way to answer “is the rootfs in here?”
Cut noisy false-positive signatures#
binwalk -x lzma -x zlib TARGET_FILE
-x drops rows whose description matches the string, one -x per class. Deep lzma or zlib “matches” inside random data are usually false; excluding them makes the real structure readable.
Carve a byte range out by hand#
binwalk TARGET_FILE # note the DECIMAL offset of the part you want
dd if=TARGET_FILE of=blob.bin bs=1 skip=OFFSET 2>/dev/null
When -e refuses to unpack a region, carve it yourself: dd copies from skip=OFFSET to the end of the file. Reliable and boring, which is exactly what you want on a stubborn blob.
Carve by rule instead of by hand#
binwalk -D 'squashfs:squashfs' TARGET_FILE
-D type:ext carves every region matching the type signature into files with the given extension, no manual dd. Add a third :cmd field to run a command on each carved file as it lands.
Spot compressed or encrypted regions with entropy#
binwalk -E -J TARGET_FILE
-E measures byte randomness; -J saves the plot as a PNG (drop -J if you have a display). Flat-high entropy means compression or encryption, flat-low means padding, and a sudden rise marks where a compressed blob begins.
Identify the CPU architecture of a raw blob#
binwalk -Y TARGET_FILE
-Y runs the capstone disassembler and reports the architecture (ARM, MIPS, and so on) it decodes cleanly. Run it before loading a headerless firmware blob into a disassembler so you pick the right CPU. -A is the lighter opcode-signature version.
Scan only part of a large file#
binwalk -o 1048576 -l 65536 TARGET_FILE
-o starts the scan at a byte offset and -l limits how many bytes to read, so you re-scan a 64 KB window around an interesting offset instead of re-processing a multi-gigabyte image.
Hunt a specific byte sequence#
binwalk -R '\x1f\x8b\x08' TARGET_FILE
-R scans for a raw byte string instead of the signature database. Here it finds every gzip header (1f 8b 08); use it when you know the exact magic you are chasing and want the offsets fast.
Save the scan for your notes#
binwalk -f scan.csv -c TARGET_FILE
sha256sum TARGET_FILE | tee notes/hash.txt
-f logs the table to a file and -c makes that log CSV, which is easy to diff between firmware versions. Hash the original first so you can prove exactly what you analyzed.
Reading the output#
| You see | Meaning | Do next |
|---|---|---|
Squashfs filesystem / JFFS2 / UBI | The root filesystem | The prize; -Me extracts it, then read the tree |
gzip / xz / LZMA compressed data | A compressed blob | -e will try to unpack it; note the offset |
uImage header / U-Boot | Bootloader or kernel image | The kernel sits right after; carve from the offset |
| A lone signature deep in random bytes | Likely a false positive | Verify with file, or -x it out; do not trust blindly |
| High entropy, no signatures | Probably encrypted | Binwalk finds it but cannot open it; move on |
| Empty table | Plain file, encrypted, or unknown format | Try -E, update Binwalk, or check file |
The DECIMAL and HEXADECIMAL columns are the same offset in two bases (feed either to dd’s skip= or to -o); DESCRIPTION names the format and often its size and parameters.
Troubleshooting#
| Problem | Cause | Fix |
|---|---|---|
command not found | Not installed / stale PATH | Reinstall; open a fresh shell |
| Detects a filesystem but will not extract | Missing unpacker | Install squashfs-tools, mtd-utils, p7zip-full |
-e produces empty or junk folders | No matching helper, or false matches | Add unpackers; carve by offset with dd |
| Empty scan table | Encrypted or unknown format | Try -E for entropy; update Binwalk for new signatures |
| Extraction fills the disk | -M recursing a huge image | Add -d to cap depth and -C to a big partition |
Permission denied writing output | Read-only or root-owned dir | Run from a writable dir, or set -C |
-E shows nothing / errors | No display for the plot | Add -J to save a PNG instead |
| Commands differ from a tutorial | 2.x Python vs 3.x Rust | Check binwalk --help banner; flags and output changed |
| Garbled or partial extracted files | Truncated or corrupt source | Re-hash the original; carve the range by offset |
Gotchas#
- A signature match is a hint, not proof. A lone row deep inside otherwise-random bytes is usually a false positive; confirm with
file,strings, or the surrounding structure before you trust it. -ewithout-Mstops at the first layer. Firmware unwraps in stages, so a plain-ehands you one compressed blob and you conclude “nothing here” when the filesystem was one recursion away.- A detected filesystem still needs its unpacker installed. Binwalk recognizes SquashFS from its own signature database but shells out to
unsquashfsandmtd-utilsto extract it; without them you get a row and no files. - High entropy means “cannot compress further,” not “compressed.” Encryption and compression both read as flat-high on the
-Egraph, so entropy alone will not tell you which; never promise decryption from a graph. - Binwalk 3.x is a different tool under the same name. The Rust rewrite changed flags, extractor config, and output, so a command copied from a 2.x guide can silently do something else. Run
binwalk --helpfirst to see which one you have. - Recursive extraction on an untrusted image can behave like a zip bomb.
-Mewill happily unpack layer after layer and fill the disk; cap it with-dand give it its own-Cdirectory on a partition you can afford to lose.
Pairs with#
file, strings, and xxd confirm and read what a scan points at before you trust the label. dd carves the exact byte range from an offset Binwalk reports, and unsquashfs or mount opens the filesystem once it is out. When the hidden data is image or audio steganography rather than an embedded format, reach for steghide or zsteg; when Binwalk stalls on a deeply nested container, unblob is the stronger recursive unpacker, and foremost or scalpel give a second opinion on pure file carving.