The demo below shows a simplified stack frame with a 4-byte buffer, a stack canary, and a saved return address. Type characters in the input field and watch the bytes fill the buffer — and then spill beyond it.
<!-- Educational buffer overflow stack frame demo -->
<div class="demo-wrap">
<p class="hint">Type into the box — each character is one byte. The stack frame holds a <b>4-byte buffer</b>, a <b>stack canary</b>, and the <b>saved return address</b>. Writing past byte 4 overwrites the canary; past byte 5 corrupts the return address.</p>
<div class="input-row">
<input id="user-input" type="text" maxlength="16" placeholder="Type here to fill the buffer…" autocomplete="off" spellcheck="false">
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<!-- Stack frame visualization — rows from low (bottom of stack) to high address -->
<div class="stack-diagram" id="stack-diagram" aria-label="Stack frame diagram">
<div class="row header-row">
<span class="label">Address</span>
<span class="label">Value</span>
<span class="label">Slot</span>
</div>
<div class="row" id="row-ret-hi">
<span class="addr">0x18</span>
<span class="cell" id="cell-ret-hi"></span>
<span class="cell-label">return address (high)</span>
</div>
<div class="row" id="row-ret-lo">
<span class="addr">0x14</span>
<span class="cell" id="cell-ret-lo"></span>
<span class="cell-label">return address (low)</span>
</div>
<div class="row divider-row">
<span class="addr">0x10</span>
<span class="cell canary-cell" id="cell-canary"></span>
<span class="cell-label">stack canary (0xDEAD)</span>
</div>
<div class="row" id="row-buf3">
<span class="addr">0x0C</span>
<span class="cell" id="cell-b3"></span>
<span class="cell-label">buf [3]</span>
</div>
<div class="row" id="row-buf2">
<span class="addr">0x08</span>
<span class="cell" id="cell-b2"></span>
<span class="cell-label">buf [2]</span>
</div>
<div class="row" id="row-buf1">
<span class="addr">0x04</span>
<span class="cell" id="cell-b1"></span>
<span class="cell-label">buf [1]</span>
</div>
<div class="row" id="row-buf0">
<span class="addr">0x00</span>
<span class="cell" id="cell-b0"></span>
<span class="cell-label">buf [0]</span>
</div>
</div>
<div class="status-bar" id="status-bar"></div>
</div>
/* Base layout and input styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.demo-wrap { max-width: 520px; margin: 0 auto; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.input-row { display: flex; gap: .5rem; margin-bottom: .9rem; }
input { flex: 1; padding: .4rem .7rem; font: 15px ui-monospace, monospace; border: 1px solid #ccc;
border-radius: 6px; outline: none; }
input:focus { border-color: #1d3557; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Stack diagram grid */
.stack-diagram { border: 1px solid #cdd9e3; border-radius: 8px; overflow: hidden; margin-bottom: .7rem; }
.row { display: grid; grid-template-columns: 56px 1fr 1fr; align-items: center;
border-bottom: 1px solid #e4eaf0; }
.header-row { background: #f0f4f8; font-size: .78rem; font-weight: 700; text-transform: uppercase;
color: #5a7088; letter-spacing: .04em; }
.header-row span { padding: .3rem .5rem; }
.row:last-child { border-bottom: none; }
.addr { font: 600 12px ui-monospace, monospace; color: #888; padding: .42rem .5rem; }
.label { padding: .3rem .5rem; }
.cell { font: 700 14px ui-monospace, monospace; padding: .42rem .5rem; min-height: 2.1rem;
display: flex; align-items: center; transition: background .15s; border-right: 1px solid #e4eaf0; }
.cell-label { font-size: .78rem; color: #5a7088; padding: .42rem .5rem; }
/* Cell state colors */
.cell.safe { background: #e6f4ea; color: #0a7d33; }
.cell.overflow { background: #fff0b0; color: #7a5800; }
.cell.canary-cell { background: #eef0ff; color: #3a4aad; }
.cell.canary-smashed { background: #fde8e8; color: #c92f3c !important; }
.cell.ret-smashed { background: #c92f3c; color: #fff !important; }
.divider-row { background: #f5f6ff; }
.status-bar { font-size: .95rem; font-weight: 600; min-height: 1.5rem; margin-top: .2rem; }
.status-bar.ok { color: #0a7d33; }
.status-bar.warn { color: #7a5800; }
.status-bar.danger { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Secret sentinel placed by the compiler between buffer and return address
const CANARY = 0xDEAD;
// Stack layout (low address at top, high at bottom in this visual)
// buf[0..3], canary(word), ret-lo(word), ret-hi(word)
const SAFE_RET_LO = 0x4008;
const SAFE_RET_HI = 0x0000;
function toHex2(n) { return '0x' + (n & 0xFF).toString(16).toUpperCase().padStart(2, '0'); }
function toHex4(n) { return '0x' + (n & 0xFFFF).toString(16).toUpperCase().padStart(4, '0'); }
// Re-render every keystroke
function update() {
const raw = document.getElementById('user-input').value;
// Convert each character to its ASCII byte value
const bytes = [];
for (let i = 0; i < raw.length; i++) bytes.push(raw.charCodeAt(i) & 0xFF);
// Fill the four buffer slots
const bufCells = ['cell-b0', 'cell-b1', 'cell-b2', 'cell-b3'];
for (let i = 0; i < 4; i++) {
const el = document.getElementById(bufCells[i]);
if (bytes[i] !== undefined) {
el.textContent = toHex2(bytes[i]);
el.className = 'cell safe';
} else {
el.textContent = '0x00';
el.className = 'cell';
}
}
// Canary slot — byte 4 (index 4) hits it
const canaryEl = document.getElementById('cell-canary');
const canaryByte = bytes[4];
if (canaryByte !== undefined) {
// Any byte that is not the expected high byte smashes the canary
const smashed = (canaryByte !== ((CANARY >> 8) & 0xFF));
canaryEl.textContent = smashed ? toHex2(canaryByte) : toHex4(CANARY);
canaryEl.className = 'cell ' + (smashed ? 'canary-smashed' : 'canary-cell');
} else {
canaryEl.textContent = toHex4(CANARY);
canaryEl.className = 'cell canary-cell';
}
// Bytes 5+ overwrite the return address
const retLoEl = document.getElementById('cell-ret-lo');
const retHiEl = document.getElementById('cell-ret-hi');
const overflowByteLo = bytes[5];
const overflowByteHi = bytes[6];
if (overflowByteLo !== undefined) {
const newLo = (overflowByteLo << 8) | (bytes[6] !== undefined ? bytes[6] : 0x08);
retLoEl.textContent = toHex4(newLo);
retLoEl.className = 'cell ret-smashed';
} else {
retLoEl.textContent = toHex4(SAFE_RET_LO);
retLoEl.className = 'cell safe';
}
if (overflowByteHi !== undefined) {
retHiEl.textContent = toHex2(overflowByteHi);
retHiEl.className = 'cell ret-smashed';
} else {
retHiEl.textContent = toHex4(SAFE_RET_HI);
retHiEl.className = 'cell safe';
}
// Status message
const bar = document.getElementById('status-bar');
const len = bytes.length;
if (len === 0) {
bar.textContent = 'Start typing to fill the buffer.';
bar.className = 'status-bar';
} else if (len <= 4) {
bar.textContent = i18nFmt('%n% byte(s) written — buffer intact.', { n: len });
bar.className = 'status-bar ok';
} else if (len === 5) {
bar.textContent = 'Byte 5 hit the canary! A real program would abort here.';
bar.className = 'status-bar warn';
} else {
bar.textContent = 'Return address corrupted — execution would be hijacked!';
bar.className = 'status-bar danger';
}
}
document.getElementById('user-input').addEventListener('input', update);
document.getElementById('btn-reset').addEventListener('click', () => {
document.getElementById('user-input').value = '';
update();
});
update();
<!-- Educational buffer overflow stack frame demo -->
<div class="demo-wrap">
<p class="hint">Type into the box — each character is one byte. The stack frame holds a <b>4-byte buffer</b>, a <b>stack canary</b>, and the <b>saved return address</b>. Writing past byte 4 overwrites the canary; past byte 5 corrupts the return address.</p>
<div class="input-row">
<input id="user-input" type="text" maxlength="16" placeholder="Type here to fill the buffer…" autocomplete="off" spellcheck="false">
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<!-- Stack frame visualization — rows from low (bottom of stack) to high address -->
<div class="stack-diagram" id="stack-diagram" aria-label="Stack frame diagram">
<div class="row header-row">
<span class="label">Address</span>
<span class="label">Value</span>
<span class="label">Slot</span>
</div>
<div class="row" id="row-ret-hi">
<span class="addr">0x18</span>
<span class="cell" id="cell-ret-hi"></span>
<span class="cell-label">return address (high)</span>
</div>
<div class="row" id="row-ret-lo">
<span class="addr">0x14</span>
<span class="cell" id="cell-ret-lo"></span>
<span class="cell-label">return address (low)</span>
</div>
<div class="row divider-row">
<span class="addr">0x10</span>
<span class="cell canary-cell" id="cell-canary"></span>
<span class="cell-label">stack canary (0xDEAD)</span>
</div>
<div class="row" id="row-buf3">
<span class="addr">0x0C</span>
<span class="cell" id="cell-b3"></span>
<span class="cell-label">buf [3]</span>
</div>
<div class="row" id="row-buf2">
<span class="addr">0x08</span>
<span class="cell" id="cell-b2"></span>
<span class="cell-label">buf [2]</span>
</div>
<div class="row" id="row-buf1">
<span class="addr">0x04</span>
<span class="cell" id="cell-b1"></span>
<span class="cell-label">buf [1]</span>
</div>
<div class="row" id="row-buf0">
<span class="addr">0x00</span>
<span class="cell" id="cell-b0"></span>
<span class="cell-label">buf [0]</span>
</div>
</div>
<div class="status-bar" id="status-bar"></div>
</div>
/* Base layout and input styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.demo-wrap { max-width: 520px; margin: 0 auto; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.input-row { display: flex; gap: .5rem; margin-bottom: .9rem; }
input { flex: 1; padding: .4rem .7rem; font: 15px ui-monospace, monospace; border: 1px solid #ccc;
border-radius: 6px; outline: none; }
input:focus { border-color: #1d3557; }
button { font: 600 13px system-ui; padding: .38rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
/* Stack diagram grid */
.stack-diagram { border: 1px solid #cdd9e3; border-radius: 8px; overflow: hidden; margin-bottom: .7rem; }
.row { display: grid; grid-template-columns: 56px 1fr 1fr; align-items: center;
border-bottom: 1px solid #e4eaf0; }
.header-row { background: #f0f4f8; font-size: .78rem; font-weight: 700; text-transform: uppercase;
color: #5a7088; letter-spacing: .04em; }
.header-row span { padding: .3rem .5rem; }
.row:last-child { border-bottom: none; }
.addr { font: 600 12px ui-monospace, monospace; color: #888; padding: .42rem .5rem; }
.label { padding: .3rem .5rem; }
.cell { font: 700 14px ui-monospace, monospace; padding: .42rem .5rem; min-height: 2.1rem;
display: flex; align-items: center; transition: background .15s; border-right: 1px solid #e4eaf0; }
.cell-label { font-size: .78rem; color: #5a7088; padding: .42rem .5rem; }
/* Cell state colors */
.cell.safe { background: #e6f4ea; color: #0a7d33; }
.cell.overflow { background: #fff0b0; color: #7a5800; }
.cell.canary-cell { background: #eef0ff; color: #3a4aad; }
.cell.canary-smashed { background: #fde8e8; color: #c92f3c !important; }
.cell.ret-smashed { background: #c92f3c; color: #fff !important; }
.divider-row { background: #f5f6ff; }
.status-bar { font-size: .95rem; font-weight: 600; min-height: 1.5rem; margin-top: .2rem; }
.status-bar.ok { color: #0a7d33; }
.status-bar.warn { color: #7a5800; }
.status-bar.danger { color: #c92f3c; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Secret sentinel placed by the compiler between buffer and return address
const CANARY = 0xDEAD;
// Stack layout (low address at top, high at bottom in this visual)
// buf[0..3], canary(word), ret-lo(word), ret-hi(word)
const SAFE_RET_LO = 0x4008;
const SAFE_RET_HI = 0x0000;
function toHex2(n) { return '0x' + (n & 0xFF).toString(16).toUpperCase().padStart(2, '0'); }
function toHex4(n) { return '0x' + (n & 0xFFFF).toString(16).toUpperCase().padStart(4, '0'); }
// Re-render every keystroke
function update() {
const raw = document.getElementById('user-input').value;
// Convert each character to its ASCII byte value
const bytes = [];
for (let i = 0; i < raw.length; i++) bytes.push(raw.charCodeAt(i) & 0xFF);
// Fill the four buffer slots
const bufCells = ['cell-b0', 'cell-b1', 'cell-b2', 'cell-b3'];
for (let i = 0; i < 4; i++) {
const el = document.getElementById(bufCells[i]);
if (bytes[i] !== undefined) {
el.textContent = toHex2(bytes[i]);
el.className = 'cell safe';
} else {
el.textContent = '0x00';
el.className = 'cell';
}
}
// Canary slot — byte 4 (index 4) hits it
const canaryEl = document.getElementById('cell-canary');
const canaryByte = bytes[4];
if (canaryByte !== undefined) {
// Any byte that is not the expected high byte smashes the canary
const smashed = (canaryByte !== ((CANARY >> 8) & 0xFF));
canaryEl.textContent = smashed ? toHex2(canaryByte) : toHex4(CANARY);
canaryEl.className = 'cell ' + (smashed ? 'canary-smashed' : 'canary-cell');
} else {
canaryEl.textContent = toHex4(CANARY);
canaryEl.className = 'cell canary-cell';
}
// Bytes 5+ overwrite the return address
const retLoEl = document.getElementById('cell-ret-lo');
const retHiEl = document.getElementById('cell-ret-hi');
const overflowByteLo = bytes[5];
const overflowByteHi = bytes[6];
if (overflowByteLo !== undefined) {
const newLo = (overflowByteLo << 8) | (bytes[6] !== undefined ? bytes[6] : 0x08);
retLoEl.textContent = toHex4(newLo);
retLoEl.className = 'cell ret-smashed';
} else {
retLoEl.textContent = toHex4(SAFE_RET_LO);
retLoEl.className = 'cell safe';
}
if (overflowByteHi !== undefined) {
retHiEl.textContent = toHex2(overflowByteHi);
retHiEl.className = 'cell ret-smashed';
} else {
retHiEl.textContent = toHex4(SAFE_RET_HI);
retHiEl.className = 'cell safe';
}
// Status message
const bar = document.getElementById('status-bar');
const len = bytes.length;
if (len === 0) {
bar.textContent = 'Start typing to fill the buffer.';
bar.className = 'status-bar';
} else if (len <= 4) {
bar.textContent = i18nFmt('%n% byte(s) written — buffer intact.', { n: len });
bar.className = 'status-bar ok';
} else if (len === 5) {
bar.textContent = 'Byte 5 hit the canary! A real program would abort here.';
bar.className = 'status-bar warn';
} else {
bar.textContent = 'Return address corrupted — execution would be hijacked!';
bar.className = 'status-bar danger';
}
}
document.getElementById('user-input').addEventListener('input', update);
document.getElementById('btn-reset').addEventListener('click', () => {
document.getElementById('user-input').value = '';
update();
});
update();
Notice what happens as you cross the buffer boundary. The first overflow byte hits the canary (a secret sentinel value placed by the compiler between the buffer and the return address). A real program checks the canary before returning; if it has changed, the process is killed immediately. Keep writing and you reach the return address itself — which is what a real exploit would overwrite to redirect execution.
Comments
Loading comments...