packer picoCTF 2024 Solution

Published: April 3, 2024

Description

Reverse this linux executable?

Local reversing

Download the out binary from the challenge artifacts.

Verify the download succeeded before reaching for upx or Ghidra.

Keep upx, strings, and Ghidra installed; strip is optional but matches the hint.

bash
wget https://artifacts.picoctf.net/c_titan/22/out
bash
ls -l out
bash
strings out | head

Solution

Want to try it yourself first?

The guided walkthrough reveals hints one step at a time.

Walk me through it
The Ghidra Reverse Engineering guide covers loading unpacked binaries and navigating entry(), and the Hex Dumps for CTF guide explains the xxd -r -p recipe used in the final step.
  1. Step 1
    Spot the packer
    Observation
    I noticed the challenge was named 'packer' and that running strings on the binary revealed 'UPX!' magic bytes and section names like UPX0 and UPX1, which are the unmistakable fingerprint of UPX packing and suggested running upx -d to decompress before any static analysis.
    strings out reveals UPX markers. Run upx -d out to unpack, then strip out to remove excess symbols (as hinted).
    bash
    upx -d out && strip out

    Expected output

                           Ultimate Packer for eXecutables
                              Copyright (C) 1996 - 2024
    UPX 4.2.2       Markus Oberhumer, Laszlo Molnar & John Reiser    Jan 3rd 2024
    
            File size         Ratio      Format      Name
       --------------------   ------   -----------   -----------
        877568 <-    366592   41.78%   linux/amd64   out
    
    Unpacked 1 file.
    What didn't work first

    Tried: Open the packed binary directly in Ghidra before running upx -d

    Ghidra disassembles the UPX decompression stub instead of the real program logic. The decompiler output shows a tight loop moving compressed bytes - none of the actual flag-handling code is visible. You must unpack with upx -d first so Ghidra sees the original ELF sections.

    Tried: Run upx -t out to test integrity instead of upx -d to decompress

    upx -t verifies that the packed file is intact but leaves it compressed. The binary still executes fine but remains opaque to static analysis. The -d flag (decompress) is required to overwrite the file with the unpacked original.

    Learn more

    UPX(Ultimate Packer for eXecutables) is an open-source executable packer that compresses a binary's code and data, prepends a small decompression stub, and produces a self-contained file that is typically 50-70% smaller than the original. At runtime, the stub decompresses the original binary into memory and jumps to it, so the packed file executes identically to the original.

    Malware authors frequently use UPX (or custom packers inspired by it) to shrink payloads and hinder static analysis - a packed binary's code section looks like compressed data to most disassemblers. The telltale signs of UPX packing visible via strings are the UPX! magic bytes and the section names UPX0 and UPX1.

    • upx -d out - decompresses in-place; the file is overwritten with the original ELF.
    • strip out - removes symbol table and debug info, further reducing size (the challenge hints at this step).
    • If the binary used a custom or modified packer, you would need to dump the process memory at runtime instead.
  2. Step 2
    Load into Ghidra
    Observation
    I noticed the binary was now a fully unpacked ELF after upx -d succeeded, which suggested loading it into Ghidra to decompile the main function and look for any string literals or encoded data that could contain the flag.
    Analyze the unpacked binary in Ghidra. Open entry() in the decompiler and scan the printf calls; the one whose string literal starts with "7069636f" (hex for "pico") is the flag, encoded as ASCII hex.
    Learn more

    Ghidra is NSA's free, open-source reverse engineering suite. It disassembles machine code into assembly and then decompiles it into readable C-like pseudocode, making it far easier to understand program logic than reading raw bytes. It supports virtually every common CPU architecture and binary format.

    The entry() function is the real start of execution in a C program - it calls library initializers and then invokes main(). In CTF binaries the flag is often embedded as a string literal in main or a function it calls, where it shows up in Ghidra's decompiled output as a long quoted constant.

    When you see a long hex literal in decompiled code it is almost always encoded data: a flag, a key, or an obfuscated string. The pattern here - a hex blob inside an executable - is one of the most common beginner reverse-engineering CTF techniques.

  3. Step 3
    Convert from hex
    Observation
    I noticed Ghidra's decompiler showed a long hex literal inside a printf call whose first bytes decoded to '7069636f' (ASCII 'pico'), which identified it as the flag encoded in plain hex and suggested piping it through xxd -r -p to recover the ASCII string.
    Pipe the hex string from Ghidra through xxd -r -p (or paste it into CyberChef's From Hex recipe). The resulting ASCII string is the flag.
    bash
    echo '<HEX_STRING_FROM_GHIDRA>' | xxd -r -p

    Expected output

    picoCTF{U9X_UnP4ck1N6_B1n4Ri3S_5de...}
    What didn't work first

    Tried: Pass the hex string to xxd without the -p flag: echo '...' | xxd -r

    xxd -r without -p expects the canonical xxd dump format (address column + grouped hex + ASCII sidebar). Feeding it a plain continuous hex string produces garbled or empty output because xxd tries to parse addresses from the first characters. The -p flag switches to plain hex mode where the input is purely continuous hex digits.

    Tried: Decode the hex with python3 -c "print(bytes.fromhex('...'))" and copy the b'...' repr as the flag

    Python's bytes.fromhex prints the bytes object repr including the b'' wrapper and escaped non-printable bytes. The actual flag string is the content inside the quotes, not the full repr. Using .decode() - for example bytes.fromhex('...').decode() - prints the clean ASCII string without the wrapper.

    Learn more

    Converting from hex to ASCII is one of the most fundamental operations in CTF challenges. Each pair of hex digits maps directly to one ASCII character: 70 is 'p', 69 is 'i', 63 is 'c', and so on. Recognizing that a string starts with 7069636f(= "pico") is a huge hint that you've found the flag encoding.

    For this challenge, the hex value Ghidra surfaces is:

    7069636f4354467b5539585f556e5034636b314e365f42316e34526933535f35646565343434317d

    Feed that value into xxd -r -p (the canonical Linux reverse-hex tool: -r reverses, -p means plain/continuous hex with no address columns) and the binary output is readable ASCII directly.

    CyberChef is an excellent alternative for interactive exploration; it also supports chained decodings, so if you encounter hex-within-base64 or similar layering, you can stack recipes to unpack everything in one view.

Interactive tools
  • Strings ExtractorPull printable text from any binary, library, or image. ASCII and UTF-16 detection, configurable minimum length, flag-like highlight, no command line needed.

Flag

Reveal flag

picoCTF{U9X_UnP4ck1N6_B1n4Ri3S_5de...}

Decoding the embedded hex string reveals the flag above.

Key takeaway

Executable packers like UPX compress a binary's code and data behind a small decompression stub, so the file looks like random bytes to a disassembler until it is unpacked. This technique is widely used by malware to defeat antivirus signatures and slow down reverse engineers, but standard packers leave recognizable magic bytes that make identification straightforward. Once unpacked, the original binary is fully visible to static analysis tools like Ghidra, which means packing adds friction rather than true protection against a determined analyst.

Related reading

Want more picoCTF 2024 writeups?

Tools used in this challenge

What to try next