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.
wget https://artifacts.picoctf.net/c/207/unpackme-upx && chmod +x unpackme-upxsudo apt install upx -yupx -d unpackme-upx -o unpackme-unpackedfile unpackme-unpackedSolution
Walk me through it- Step 1Unpack the UPX binaryRun
upx -d unpackme-upxto decompress the binary in-place (or use -o to write a new file). The decompressed ELF can then be analyzed normally.bashupx -d unpackme-upx -o unpackme-unpackedbashls -lh unpackme-upx unpackme-unpackedLearn 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-dflag decompresses;-llists packing info. Runningstringson a packed binary shows mostly garbage; after unpacking, meaningful strings like function names and the password comparison become visible. - Step 2Find the password in GhidraOpen 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
mainsymbol). The standard trick is to find the call to__libc_start_main: the very first argument to it is a function pointer tomain. 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)orstrncmp. 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 juststrings -n 8the binary, then look for the line whose neighbors includepicoCTF{or a clearly dictionary-style password. - Step 3Run the binary with the correct passwordEnter the hardcoded password when prompted (e.g., '230d4acf'). The binary verifies it and prints the flag.bash
./unpackme-unpackedbash# 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-unpackedtraces library calls and will show the arguments tostrcmp()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
strcmpis 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) andltrace(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.