Description
Don't just analyze this binary statically - use dynamic analysis to find the flag. The binary validates input at runtime.
Setup
Download the binary and make it executable.
wget https://mercury.picoctf.net/static/.../challchmod +x challSolution
Want to try it yourself first?
The guided walkthrough reveals hints one step at a time.
Step 1
Try ltrace - it usually leaks the comparison directlyObservationI noticed the challenge is titled 'Let's Get Dynamic' and the description explicitly says to use dynamic analysis, which suggested intercepting library calls at runtime with ltrace since dynamically linked binaries pass strcmp arguments in plaintext to the trace.Run the binary under ltrace with dummy input. A line shaped like strcmp("your_input", "the_flag") = 1 reveals the expected value without any reverse engineering. Fall back to strace if the check is implemented as a syscall or read-from-file.bashltrace ./chall <<< 'picoCTF{test}' 2>&1 | head -50bashltrace -e strcmp+memcmp+strncmp ./chall <<< 'picoCTF{test}' 2>&1bashstrace -e openat,read ./chall <<< 'picoCTF{test}' 2>&1 | head -50Expected output
strcmp("picoCTF{test}", "picoCTF{dyn4m1c_4n4ly1s_1s_5up3r_us3ful_...}") = -1What didn't work first
Tried: Run strings on the binary instead of ltrace, hoping the flag is stored as a plain string.
strings ./chall will list printable byte sequences but won't necessarily surface the flag if it is assembled at runtime, stored as separate character arrays, or XOR-encoded. ltrace intercepts the actual strcmp call after any in-memory assembly happens, so the decrypted or concatenated value is visible there even when strings produces nothing useful.
Tried: Use ltrace without filtering and miss the comparison line in pages of output.
Unfiltered ltrace on a verbose binary can print hundreds of memory-allocation and I/O calls, pushing the strcmp line off the visible buffer. Adding -e strcmp+memcmp+strncmp narrows the trace to comparison functions only, making the flag-revealing line the first - and usually only - thing printed.
Learn more
Dynamic analysis tools observe a program's behavior while it is running, rather than reading the binary statically. This is powerful because it bypasses obfuscation: no matter how complex the code, the final comparison must actually happen at runtime, and tracing tools can intercept it.
strace captures system calls - interactions between the program and the kernel. Useful for finding file reads (
openat,read), network activity (connect,send), and process control (fork,execve).ltrace captures calls to shared library functions (libc, libssl, etc.). Useful for finding
strcmp,memcmp, crypto functions, and other library-level comparisons that reveal expected values.Step 2
Use GDB to intercept the comparisonObservationI noticed ltrace may produce no output if the binary is statically linked (no shared library calls to intercept), which suggested falling back to GDB to set a breakpoint on the memcmp or strcmp site and read the expected flag value from the $rsi register at the moment the comparison fires.If ltrace does not reveal the comparison (e.g., the binary is statically linked), load it in GDB and set breakpoints on comparison instructions. Inspect registers when the breakpoint fires.bashgdb -q ./challbash# (gdb) catch syscall readbash# (gdb) break memcmpbash# (gdb) run 'picoCTF{test}'bash# At breakpoint: x/s $rdi, x/s $rsiWhat didn't work first
Tried: Set a breakpoint on strcmp by name in GDB expecting it to work like ltrace even on a statically linked binary.
In a dynamically linked binary GDB resolves strcmp via the PLT and the breakpoint fires correctly. In a statically linked binary the symbol may not exist as a named export, so 'break strcmp' produces 'Function not defined'. The fix is to locate the inlined copy with 'nm chall | grep strcmp' or 'info functions strcmp' and break on the raw address instead.
Tried: Read flag bytes from $rdi instead of $rsi at the memcmp breakpoint, and see the user-supplied input rather than the expected value.
By x86-64 calling convention the first argument is in $rdi and the second is in $rsi. memcmp(user_input, expected, len) places the user's string in $rdi, which is whatever you typed. The expected flag sits in $rsi. Printing 'x/s $rsi' is what reveals the target value.
Learn more
Breakpoints on common comparison functions: Even in statically linked binaries,
memcmpand similar functions from the C library are still present in the binary (not as dynamic imports, but as compiled-in functions). You can break on them by address after finding them in the symbol table (nm chall | grep memcmp) or by setting breakpoints on the disassembled addresses where the comparison occurs.GDB scripting: For comparisons done in a loop (character by character), use a GDB Python script or commands script to automate: break on the comparison instruction, print the expected byte, continue, and repeat until the full flag is assembled.
Flag
Reveal flag
picoCTF{dyn4m1c_4n4ly1s_1s_5up3r_us3ful_...}
Dynamic analysis (ltrace/strace/GDB) intercepts the comparison at runtime regardless of static obfuscation - the correct value must be present in memory at the moment of comparison.