unpackme picoCTF 2022 Solution

Published: July 20, 2023

Description

A UPX-packed ELF binary hides its real code behind runtime decompression. Unpack it with upx -d, then analyze the unpacked binary in Ghidra to find a hardcoded password comparison.

Download the binary and make it executable.

Install UPX if not already available: sudo apt install upx.

Unpack the binary, then analyze in Ghidra.

bash
wget https://artifacts.picoctf.net/c/207/unpackme-upx && chmod +x unpackme-upx
bash
sudo apt install upx -y
bash
upx -d unpackme-upx -o unpackme-unpacked
bash
file unpackme-unpacked
  1. Step 1Unpack the UPX binary
    Run upx -d unpackme-upx to decompress the binary in-place (or use -o to write a new file). The decompressed ELF can then be analyzed normally.
    bash
    upx -d unpackme-upx -o unpackme-unpacked
    bash
    ls -lh unpackme-upx unpackme-unpacked
    Learn more

    UPX (Ultimate Packer for eXecutables) is a free, open-source packer that compresses executable files. At runtime, the UPX stub decompresses the original code into memory and jumps to the original entry point. Packed binaries are smaller on disk but unpack to their full size in memory.

    Malware commonly uses UPX (or custom packers) to evade antivirus signature scanning - the raw bytes of the packed file don't match the signatures for the malicious code inside. Analysts unpack first, then scan or reverse engineer the inner binary.

    UPX stores a magic header (UPX!) at the end of compressed segments. The -d flag decompresses; -l lists packing info. Running strings on a packed binary shows mostly garbage; after unpacking, meaningful strings like function names and the password comparison become visible.

  2. Step 2Find the password in Ghidra
    Open the unpacked binary in Ghidra. Navigate to main() or the password-check function and look for the strcmp or strncmp call comparing user input to a hardcoded string.
    Learn more

    Ghidra is NSA's free reverse engineering framework. After importing the binary and running auto-analysis, use the Symbol Tree panel to navigate directly to main. Ghidra's decompiler (press F) converts the disassembly into readable C pseudocode.

    Finding main in a stripped binary. UPX-unpacked binaries are usually stripped (no main symbol). The standard trick is to find the call to __libc_start_main: the very first argument to it is a function pointer to main. Two ways:

    # objdump way: print the disassembly around every __libc_start_main call.
    objdump -d unpackme-unpacked | grep -B5 '__libc_start_main'
    
    # Look for the immediately-preceding instruction that loads RDI (System V x86-64),
    # e.g. "lea rdi, [rip+0xNNN]" -> RIP-relative address of main.
    
    # In Ghidra: open the entry function (usually _start), follow the first argument
    # loaded into RDI before the call __libc_start_main, double-click the address.

    Look for a function call that compares two strings, typically strcmp(user_input, hardcoded_string) or strncmp. The hardcoded string is the password. You can also search for string literals directly: Search > For Strings, then filter for short alphanumeric strings near the length you expect.

    Why the regex filter. strings -n 8 unpackme-unpacked | grep -E '^[a-zA-Z0-9_{}]+$' ignores low-information output (relocation tables, glibc strings, format directives) and keeps only the kind of identifier-shaped strings UPX challenges tend to use as passwords. It is a heuristic, not a rule: if it returns nothing, drop the regex and just strings -n 8 the binary, then look for the line whose neighbors include picoCTF{ or a clearly dictionary-style password.

  3. Step 3Run the binary with the correct password
    Enter the hardcoded password when prompted (e.g., '230d4acf'). The binary verifies it and prints the flag.
    bash
    ./unpackme-unpacked
    bash
    # Enter password: 230d4acf (or whatever Ghidra reveals)
    Learn more

    Once you have the password from static analysis, simply run the binary normally and provide it when prompted. The comparison succeeds and the flag is printed.

    An alternative to static analysis is dynamic analysis with ltrace: ltrace ./unpackme-unpacked traces library calls and will show the arguments to strcmp() directly, including both the user input and the hardcoded password side by side. A worked sample looks like:

    $ ltrace ./unpackme-unpacked
    __libc_start_main(0x401234, 1, 0x7ffe..., 0x401300 ...
    puts("Enter password: ")        = 17
    fgets(0x7ffe...,  64, 0x7f...)  = 0x7ffe...   <- you type "test"
    strcmp("test\n", "230d4acf")    = -49        <- hardcoded password leaked!
    puts("Wrong.")                  = 7
    +++ exited (status 0) +++

    The second argument to strcmp is the embedded password. Re-run the binary with that value as input and the flag prints. This works even on packed binaries (ltrace hooks the runtime), but the output is empty until you enter something to trigger the comparison.

    strace (system call trace) and ltrace (library call trace) are essential dynamic analysis tools for understanding what a binary does without fully reversing it. They are commonly combined with GDB for a complete picture.

Flag

picoCTF{unp4ck_1t_9...}

Unpack with `upx -d`, find the password in Ghidra's decompiled main(), then run the binary with the correct input.

Want more picoCTF 2022 writeups?

Tools used in this challenge

Related reading

What to try next