Skip to content
Courses

Program Investigation Capstone

Course CompleteIndependent capstone3–4 hoursLabBuilds on: Explain the Wrong Report

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 program-evidence-v1.0.0.tar into ~/underdunn-course/downloads.

Verify the release digest:

Terminal window
cd ~/underdunn-course/downloads
sha256sum program-evidence-v1.0.0.tar

Expected digest:

d1e7974490f49c845a1ca8486949e363ba3effaf8cb821d8ef459233ba8b15e0 program-evidence-v1.0.0.tar

Don’t continue if it differs. Download a fresh copy and verify it again.

Extract a working copy:

Terminal window
mkdir -p ~/underdunn-course/labs/program-evidence
tar -xf program-evidence-v1.0.0.tar -C ~/underdunn-course/labs/program-evidence
cd ~/underdunn-course/labs/program-evidence/program-evidence-v1.0.0
sha256sum -c CHECKSUMS.sha256

Every listed file has to report OK there, with no exceptions.

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:

Terminal window
gcc -std=c11 -Wall -Wextra -Wpedantic -O0 -g -fno-pie -no-pie \
-Wl,--build-id=none -o program-evidence-rebuilt program-evidence.c
sha256sum program-evidence program-evidence-rebuilt
file program-evidence program-evidence-rebuilt

Those 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.

Terminal window
readelf -h program-evidence
readelf -lW program-evidence | grep LOAD

Check 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.

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:

Terminal window
./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:

Terminal window
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:

Terminal window
kill "$CAPSTONE_PID"
wait "$CAPSTONE_PID" 2>/dev/null || true
unset CAPSTONE_PID

Milestone 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:

Terminal window
objdump -d -Mintel --disassemble=classify_message program-evidence
objdump -d -Mintel --disassemble=write_report program-evidence

In 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.

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:

Terminal window
rm -f program-evidence-output.txt trace.txt
timeout 3s strace -o trace.txt -e trace=openat,write,close,clock_nanosleep \
./program-evidence evidence
grep 'program-evidence-output.txt' trace.txt
grep -E 'write\(|clock_nanosleep' trace.txt

timeout 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.

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:

Terminal window
cp program-evidence.c program-evidence-modified.c

In 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:

Terminal window
gcc -std=c11 -Wall -Wextra -Wpedantic -O0 -g -fno-pie -no-pie \
-Wl,--build-id=none -o program-evidence-modified program-evidence-modified.c
timeout 3s ./program-evidence-modified evidence
cat program-evidence-output.txt

Check 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:

Terminal window
gdb -q ./program-evidence

Then enter GDB input:

break classify_message
break write_report
run evidence
print length
finish
continue
print classification

The 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.

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.

Check your answers

Enter the values you found. Your answers are checked in this browser without being sent anywhere.

These fields check the same two observations. Use the milestones above to check the rest of the investigation; notes are optional.

Hint 1: a process exits before map inspectionUse 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 mapsCompare 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 thresholdRead the named enum constant near the top of program-evidence.c, and don’t try to infer it from an unexplained byte sequence.
  • 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/.../maps is 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.txt before 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.

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.

If CAPSTONE_PID is still set, verify it before you stop it. Then remove only the material this capstone created:

Terminal window
cd ~/underdunn-course/labs/program-evidence
rm -rf program-evidence-v1.0.0
rm -f ~/underdunn-course/downloads/program-evidence-v1.0.0.tar
unset CAPSTONE_PID

You’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.