Program Investigation Capstone
Prerequisites and supported environment
Section titled “Prerequisites and supported environment”Finish Programming Foundations, How Programs Work, and Debugging Foundations, including the Executable Mapping and Debugging Report labs, before you start. Work in the Ubuntu 24.04.4 LTS x86-64 course VM, with GCC 13.x, GNU binutils 2.42, strace 6.8, and GDB 15.x.
The archive includes the full C source, a build script, the matching executable, and internal checksums. Keep that source and executable together throughout, since the whole point is comparing them. This investigation is asking you to correlate known behavior across several representations (there’s nothing hidden to reverse-engineer here).
Download and establish identity
Section titled “Download and establish identity”Download program-evidence-v1.0.0.tar into ~/underdunn-course/downloads.
Verify the release digest:
cd ~/underdunn-course/downloadssha256sum program-evidence-v1.0.0.tarExpected digest:
d1e7974490f49c845a1ca8486949e363ba3effaf8cb821d8ef459233ba8b15e0 program-evidence-v1.0.0.tarDon’t continue if it differs. Download a fresh copy and verify it again.
Extract a working copy:
mkdir -p ~/underdunn-course/labs/program-evidencetar -xf program-evidence-v1.0.0.tar -C ~/underdunn-course/labs/program-evidencecd ~/underdunn-course/labs/program-evidence/program-evidence-v1.0.0sha256sum -c CHECKSUMS.sha256Every listed file has to report OK there, with no exceptions.
Milestone 1: connect source to the build
Section titled “Milestone 1: connect source to the build”Explain how the supplied source and build settings relate to the executable. Predict the behavior on either side of the message-length boundary, then choose a way to build and compare a separate executable.
Hint 1: strategy
Separate source-level behavior from build identity. A changed checksum alone does not tell you which behavior changed.
Hint 2: tools and evidence locations
Use the build script, your compiler, a checksum tool, and an executable identification tool.
Hint 3: one command walkthrough
Read program-evidence.c and build.sh first, and don’t inspect the executable yet.
Predict:
- The output class for a seven-character message and an eight-character message.
- The filename the program will create.
- Why the program remains alive after printing.
- Whether the supplied executable should contain symbol and debug information.
Build a separate comparison executable:
gcc -std=c11 -Wall -Wextra -Wpedantic -O0 -g -fno-pie -no-pie \ -Wl,--build-id=none -o program-evidence-rebuilt program-evidence.csha256sum program-evidence program-evidence-rebuiltfile program-evidence program-evidence-rebuiltThose hashes may well differ if your local toolchain records different compiler metadata, and that’s fine. Both file results have to identify x86-64 dynamically linked ELF executables that aren’t stripped. Check the exact evidence you got, rather than claiming a byte-for-byte identity you haven’t actually established.
Milestone 2: predict and inspect the load view
Section titled “Milestone 2: predict and inspect the load view”Establish the executable’s architecture, file type, entry point, and load requirements. Predict permissions before inspecting the load view, and explain which evidence establishes each claim.
Hint 1: strategy
The header identifies the file. The load view describes regions and permissions; neither is a live process map.
Hint 2: tools and evidence locations
Choose the relevant readelf views and inspect the full rows needed to interpret the load requirements.
Hint 3: one command walkthrough
Before you run readelf, predict how many LOAD rows there’ll be, and which permission combinations are going to appear.
readelf -h program-evidencereadelf -lW program-evidence | grep LOADCheck the ELF class, the machine, the type, the entry point, the exact LOAD count, and the permissions. Then explain why write permission and execute permission allow different operations.
Milestone 3: correlate a live process
Section titled “Milestone 3: correlate a live process”Run the supplied executable with an input you choose, verify its identity, and correlate its live mappings with the file. Compare the map with the permissions you expected.
Hint 1: strategy
Use one verified PID for these inspections. The supplied program stays alive for 120 seconds; prepare your inspection plan before starting it. If it exits, restart and repeat the inspection.
Hint 2: tools and evidence locations
Use the shell’s recorded background PID, process inspection, and /proc. Compare backing-file offsets and permissions with the ELF view.
Hint 3: one command walkthrough
Start the supplied program with an eight-character message and save its PID immediately:
./program-evidence evidence > run-output.txt 2> run-errors.txt &CAPSTONE_PID=$!printf 'recorded pid: %s\n' "$CAPSTONE_PID"ps -p "$CAPSTONE_PID" -o pid=,stat=,comm=That ps row has to name program-eviden, or whatever the platform-truncated form of the executable name turns out to be. If no row appears at all, restart the process and replace CAPSTONE_PID before you continue.
Predict which mapping permission sets will correspond to the ELF LOAD rows, and then inspect only the mappings for this executable:
grep program-evidence "/proc/$CAPSTONE_PID/maps"grep '\[heap\]' "/proc/$CAPSTONE_PID/maps"Correlate the file offsets and permission sets against your earlier program-header evidence. Check the address values as observations from this one run, and don’t treat any of them as universal constants.
Stop only the verified process:
kill "$CAPSTONE_PID"wait "$CAPSTONE_PID" 2>/dev/null || trueunset CAPSTONE_PIDMilestone 4: connect a named function to instructions
Section titled “Milestone 4: connect a named function to instructions”Choose source-visible functions that let you explain a decision and an output operation. Correlate their C constructs with actual instructions and justify your selections.
Hint 1: strategy
Trace values and branches, and keep instruction addresses separate from the source expressions they implement.
Hint 2: tools and evidence locations
Use focused disassembly for the symbols classify_message and write_report. These are supplied source names.
Hint 3: one command walkthrough
Use the source-visible names here, rather than hunting around for hidden behavior:
objdump -d -Mintel --disassemble=classify_message program-evidenceobjdump -d -Mintel --disassemble=write_report program-evidenceIn classify_message, find the comparison associated with LONG_MESSAGE_THRESHOLD, and the control-flow choice selecting long or short. In write_report, find at least two library calls corresponding to operations you can see in the source.
Your conclusion has to name the source construct and the instruction evidence together. An instruction address locates the evidence; your explanation connects the operation there to the source behavior.
Milestone 5: observe the system boundary
Section titled “Milestone 5: observe the system boundary”Observe the kernel services used to create the report and wait. Explain a descriptor, return value, and the limits of your trace. Choose a bounded observation window and preserve any expected termination status.
Hint 1: strategy
Distinguish the library operations in the source from the system calls you actually observe. Ensure the output belongs to this run.
Hint 2: tools and evidence locations
Use strace with relevant calls selected, and timeout for a short run. Remove only stale exercise output before tracing another run.
Hint 3: one command walkthrough
Remove any stale output first, and then trace one short run. timeout closes the observation window for you, so you aren’t sitting there for two minutes:
rm -f program-evidence-output.txt trace.txttimeout 3s strace -o trace.txt -e trace=openat,write,close,clock_nanosleep \ ./program-evidence evidencegrep 'program-evidence-output.txt' trace.txtgrep -E 'write\(|clock_nanosleep' trace.txttimeout usually reports status 124, because it’s deliberately ending a sleeping process. That status is entirely expected here.
Check the evidence for opening the report, writing the bytes, closing a descriptor, and entering the sleep. Keep the C library calls you see in the source distinct from the Linux system calls strace shows you.
Milestone 6: change one source constant
Section titled “Milestone 6: change one source constant”In a separate source copy, choose a different message threshold and an input that distinguishes the original from the changed behavior. Predict the difference, rebuild, and explain the narrow conclusion supported by your result.
Hint 1: strategy
Change one constant and preserve the baseline. A useful test crosses the changed boundary while keeping other input properties fixed.
Hint 2: tools and evidence locations
Use a source copy, the documented build settings, and a comparison of the report output. The walkthrough uses threshold 10 as one example.
Hint 3: one command walkthrough
Copy the source:
cp program-evidence.c program-evidence-modified.cIn that copy only, change LONG_MESSAGE_THRESHOLD to 10, having read its original value out of the source back in Milestone 1. Predict the class for evidence, and then build and observe:
gcc -std=c11 -Wall -Wextra -Wpedantic -O0 -g -fno-pie -no-pie \ -Wl,--build-id=none -o program-evidence-modified program-evidence-modified.ctimeout 3s ./program-evidence-modified evidencecat program-evidence-output.txtCheck the changed constant, your prediction, the output, and the smallest conclusion the difference actually supports. The supplied executable stays unchanged throughout, so you’ve always got something to compare against.
Milestone 7: test the source explanation against live state
Section titled “Milestone 7: test the source explanation against live state”Use the unmodified source and executable again. Choose two inputs on opposite sides of the original classification boundary and predict their lengths and classifications. Select a stopping point that lets you test the source-level decision against live state, then justify why the frame and variable are valid at that stop.
Check the build and input, breakpoint choice, predicted and observed argument, the function’s returned classification, and one connection to the instruction evidence from Milestone 4. This is an observation task, not a request to change registers or force the program down a different path. Do not treat a stopped process as evidence that later file operations completed.
Hint 1: choose a question before a command
The source has one function deciding a classification and another receiving it for output. Compare the decision’s input with the value handed to the output function. Keep both observations tied to one run.
Hint 2: use familiar stopping points
GDB can stop at classify_message and write_report. Inspect the source-named argument in each frame and observe a real return with finish. The return type is a string pointer, not the integer used in the earlier classify example; explain the value using its source type.
Hint 3: one possible comparison
From the capstone directory, start the baseline in the shell:
gdb -q ./program-evidenceThen enter GDB input:
break classify_messagebreak write_reportrun evidenceprint lengthfinishcontinueprint classificationThe first stop is in the classifier; finish executes its real return. The next continue reaches the output function, where classification is a valid argument to inspect. GDB prints a character pointer with its string contents when available. Compare that result with your prediction rather than reporting the pointer address as a classification.
Restart with a seven-character input you choose and repeat the comparison. GDB asks before replacing the currently paused run; answer y when that is your intention. Quit afterward, confirming termination of only this debugger-launched process. Keep the two runs’ state separate from earlier /proc and strace sessions.
If a name is unavailable, check the selected frame and build. If you followed the walkthrough, repeat with a fresh input and your own observation choice before marking this milestone independent.
Check two capstone observations
Section titled “Check two capstone observations”Enter two observations, one from the supplied source and one from the trace:
- The original long-message threshold.
- The system call that sends report bytes to an open file descriptor.
These fields check the same two observations. Use the milestones above to check the rest of the investigation; notes are optional.
Troubleshooting hints
Section titled “Troubleshooting hints”Hint 1: a process exits before map inspection
Use the supplied executable, save$! in a shell variable immediately, and verify the PID with ps before reading /proc. The source keeps a valid run alive for 120 seconds.Hint 2: connect ELF rows to maps
Compare the program-header file offset and permissions with the executable-backed rows in/proc/$CAPSTONE_PID/maps. Page alignment can produce more map rows than LOAD rows.Hint 3: locate the public threshold
Read the named enum constant near the top ofprogram-evidence.c, and don’t try to infer it from an unexplained byte sequence.Common failures and recovery
Section titled “Common failures and recovery”- The archive or an internal checksum doesn’t match: delete the affected download or extracted directory, download it again, and reverify before you analyze anything.
- Compiler warnings or errors appear: restore the exact supplied source and command, because the baseline build has to complete without a single warning.
/proc/.../mapsis missing: the process ended, or the PID is wrong. Start it again and verify the new PID before you read any maps.- The addresses differ from an earlier run: use the current values. Compare offsets and permissions rather than memorized process addresses.
strace: command not found: go back to Prepare Workspace and install the pinned course tool set before continuing.- The trace contains old rows: remove
trace.txtbefore each traced run. - A capstone process is still alive: verify its command with
ps -p PID -o pid=,comm=before you send it any signal.
Check your result
Section titled “Check your result”The checker should accept both observations. Check that the changed threshold changes the boundary case, and that the source, debugger state and printed result agree about which branch ran. The file-writing call in your trace should use the same output file as the program. If a view disagrees, check the build, input and PID before changing the explanation. Stop any remaining capstone process when finished.
Reset and cleanup
Section titled “Reset and cleanup”If CAPSTONE_PID is still set, verify it before you stop it. Then remove only the material this capstone created:
cd ~/underdunn-course/labs/program-evidencerm -rf program-evidence-v1.0.0rm -f ~/underdunn-course/downloads/program-evidence-v1.0.0.tarunset CAPSTONE_PIDYou’ve connected an executable with its source, its live state and its requests to Linux. Continue to the course review to find any part you want to revisit.