Below is a simplified memory model. The gadget library on the left shows available fragments — each ends with ret. Your stack starts empty. Drag gadgets onto the stack (or click Add) to build a chain that reaches the goal shown at the top.
<!-- ROP gadget chain demo — build a chain from the library on the left -->
<div class="goal-bar">
<span class="goal-label">Goal:</span>
<span id="goal-text" class="goal-text"></span>
</div>
<div class="panels">
<div class="panel gadget-panel">
<div class="panel-title">Gadget Library</div>
<div id="gadget-list" class="gadget-list"></div>
</div>
<div class="panel stack-panel">
<div class="panel-title">Stack (your chain) <span class="stack-arrow">↓ grows down</span></div>
<div id="stack-list" class="stack-list"></div>
<div class="stack-empty" id="stack-empty">Stack is empty — add gadgets above</div>
</div>
</div>
<div class="state-bar">
<span class="state-label">Registers:</span>
<span id="reg-display" class="reg-display"></span>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="btn-run" type="button">Run chain</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* base reset */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; font-size: 14px; }
.goal-bar { display: flex; align-items: baseline; gap: .4rem; background: #f0f4f8;
border: 1px solid #cdd9e3; border-radius: 8px; padding: .5rem .8rem; margin-bottom: .7rem; }
.goal-label { font-weight: 700; color: #1d3557; white-space: nowrap; }
.goal-text { font-family: ui-monospace, monospace; font-size: .85rem; color: #444; }
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; margin-bottom: .6rem; }
.panel { border: 1px solid #cdd9e3; border-radius: 8px; overflow: hidden; }
.panel-title { background: #1d3557; color: #fff; font-weight: 700; font-size: .8rem;
padding: .35rem .7rem; display: flex; align-items: center; justify-content: space-between; }
.stack-arrow { font-weight: 400; font-size: .75rem; opacity: .8; }
.gadget-list, .stack-list { padding: .4rem; display: flex; flex-direction: column; gap: .3rem; min-height: 160px; }
.stack-empty { color: #999; font-size: .8rem; text-align: center; padding: .4rem; display: none; }
/* gadget row style */
.gadget-item { display: flex; align-items: center; gap: .4rem; background: #e8eef3;
border: 1px solid #bac4cc; border-radius: 6px; padding: .3rem .5rem; }
.gadget-code { font-family: ui-monospace, monospace; font-size: .8rem; color: #1d3557; flex: 1; }
.gadget-desc { font-size: .75rem; color: #555; }
.gadget-btn { font-size: .75rem; padding: .15rem .4rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 4px; cursor: pointer; white-space: nowrap; }
/* stack row styles — executed (green), current (yellow highlight) */
.stack-item { display: flex; align-items: center; gap: .4rem; background: #fff3cd;
border: 1px solid #f0c040; border-radius: 6px; padding: .3rem .5rem; }
.stack-addr { font-family: ui-monospace, monospace; font-size: .75rem; color: #888; min-width: 56px; }
.stack-code { font-family: ui-monospace, monospace; font-size: .8rem; color: #333; flex: 1; }
.stack-rem { font-size: .75rem; padding: .1rem .35rem; border: 1px solid #c92f3c;
background: transparent; color: #c92f3c; border-radius: 4px; cursor: pointer; }
.stack-item.executed { background: #d4edda; border-color: #6dbd8a; }
.stack-item.current { background: #ffeeba; border-color: #e0a800; box-shadow: 0 0 0 2px #e0a800; }
/* register state bar */
.state-bar { display: flex; align-items: baseline; gap: .5rem; margin-bottom: .4rem;
padding: .4rem .7rem; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; }
.state-label { font-weight: 700; color: #1d3557; white-space: nowrap; }
.reg-display { font-family: ui-monospace, monospace; font-size: .8rem; color: #333; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Available gadgets — each ends with ret, transferring control to the next address on the stack.
// In a real attack these come from existing library code; here they are simplified for clarity.
const GADGETS = [
{ id: 'pop_eax', code: 'pop eax ; ret', effect: s => { s.eax = 1; }, desc: 'sets eax = 1' },
{ id: 'pop_ebx', code: 'pop ebx ; ret', effect: s => { s.ebx = 2; }, desc: 'sets ebx = 2' },
{ id: 'add_eax', code: 'add eax, ebx ; ret', effect: s => { s.eax += s.ebx; }, desc: 'eax = eax + ebx' },
{ id: 'mov_mem', code: 'mov [mem], eax ; ret', effect: s => { s.mem = s.eax; }, desc: 'writes eax into memory' },
{ id: 'xor_eax', code: 'xor eax, eax ; ret', effect: s => { s.eax = 0; }, desc: 'zeroes eax' },
];
// Goal: reach eax=3 and mem=3 using only gadgets (pop eax, pop ebx, add, then mov).
const GOAL = { eax: 3, mem: 3 };
let chain = [];
let state = { eax: 0, ebx: 0, mem: null };
let executed = 0;
function goalText() {
return 'eax = ' + GOAL.eax + ', mem = ' + GOAL.mem;
}
function regText(s) {
return 'eax=' + s.eax + ' ebx=' + s.ebx + ' [mem]=' + (s.mem === null ? '?' : s.mem);
}
function goalMet(s) {
return s.eax === GOAL.eax && s.mem === GOAL.mem;
}
function renderGadgets() {
const el = document.getElementById('gadget-list');
el.innerHTML = '';
GADGETS.forEach(g => {
const row = document.createElement('div');
row.className = 'gadget-item';
row.innerHTML =
'<span class="gadget-code">' + g.code + '</span>' +
'<span class="gadget-desc">' + g.desc + '</span>' +
'<button class="gadget-btn" data-id="' + g.id + '">Add</button>';
el.appendChild(row);
});
el.querySelectorAll('.gadget-btn').forEach(btn => {
btn.addEventListener('click', () => {
const g = GADGETS.find(x => x.id === btn.dataset.id);
if (g) { chain.push(g); executed = 0; state = { eax: 0, ebx: 0, mem: null }; renderStack(); renderRegs(); setStatus(''); }
});
});
}
function renderStack() {
const el = document.getElementById('stack-list');
const empty = document.getElementById('stack-empty');
el.innerHTML = '';
if (chain.length === 0) { empty.style.display = 'block'; return; }
empty.style.display = 'none';
// render each chain entry with address, code, and a remove button
chain.forEach((g, i) => {
const addr = '0x' + (0xbfff0000 + i * 4).toString(16).toUpperCase();
const row = document.createElement('div');
let cls = 'stack-item';
if (i < executed) cls += ' executed';
else if (i === executed && executed < chain.length) cls += ' current';
row.className = cls;
row.innerHTML =
'<span class="stack-addr">' + addr + '</span>' +
'<span class="stack-code">' + g.code + '</span>' +
'<button class="stack-rem" data-idx="' + i + '">✕</button>';
el.appendChild(row);
});
el.querySelectorAll('.stack-rem').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx, 10);
chain.splice(idx, 1);
executed = 0; state = { eax: 0, ebx: 0, mem: null };
renderStack(); renderRegs(); setStatus('');
});
});
}
function renderRegs() {
document.getElementById('reg-display').textContent = regText(state);
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg; el.className = 'status ' + (cls || '');
}
function runChain() {
if (chain.length === 0) { setStatus('Add gadgets to the stack first.', 'bad'); return; }
// execute each gadget in order, animating one step at a time
state = { eax: 0, ebx: 0, mem: null };
executed = 0;
renderStack(); renderRegs();
const step = () => {
if (executed >= chain.length) {
if (goalMet(state)) {
setStatus(i18nFmt('Goal reached in %n% gadget(s)! No new code was injected.', { n: chain.length }), 'ok');
} else {
setStatus('Chain finished but goal not met — try a different sequence.', 'bad');
}
renderStack();
return;
}
const g = chain[executed];
g.effect(state);
executed++;
renderRegs();
renderStack();
setStatus(i18nFmt('Executing gadget %step% / %total%: %code%', { step: executed, total: chain.length, code: g.code }), 'info');
setTimeout(step, 420);
};
step();
}
document.getElementById('btn-run').addEventListener('click', runChain);
document.getElementById('btn-reset').addEventListener('click', () => {
chain = []; executed = 0; state = { eax: 0, ebx: 0, mem: null };
renderStack(); renderRegs(); setStatus('');
});
document.getElementById('goal-text').textContent = goalText();
renderGadgets();
renderStack();
renderRegs();
<!-- ROP gadget chain demo — build a chain from the library on the left -->
<div class="goal-bar">
<span class="goal-label">Goal:</span>
<span id="goal-text" class="goal-text"></span>
</div>
<div class="panels">
<div class="panel gadget-panel">
<div class="panel-title">Gadget Library</div>
<div id="gadget-list" class="gadget-list"></div>
</div>
<div class="panel stack-panel">
<div class="panel-title">Stack (your chain) <span class="stack-arrow">↓ grows down</span></div>
<div id="stack-list" class="stack-list"></div>
<div class="stack-empty" id="stack-empty">Stack is empty — add gadgets above</div>
</div>
</div>
<div class="state-bar">
<span class="state-label">Registers:</span>
<span id="reg-display" class="reg-display"></span>
</div>
<div class="status" id="status"></div>
<div class="btns">
<button id="btn-run" type="button">Run chain</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* base reset */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; font-size: 14px; }
.goal-bar { display: flex; align-items: baseline; gap: .4rem; background: #f0f4f8;
border: 1px solid #cdd9e3; border-radius: 8px; padding: .5rem .8rem; margin-bottom: .7rem; }
.goal-label { font-weight: 700; color: #1d3557; white-space: nowrap; }
.goal-text { font-family: ui-monospace, monospace; font-size: .85rem; color: #444; }
.panels { display: grid; grid-template-columns: 1fr 1fr; gap: .6rem; margin-bottom: .6rem; }
.panel { border: 1px solid #cdd9e3; border-radius: 8px; overflow: hidden; }
.panel-title { background: #1d3557; color: #fff; font-weight: 700; font-size: .8rem;
padding: .35rem .7rem; display: flex; align-items: center; justify-content: space-between; }
.stack-arrow { font-weight: 400; font-size: .75rem; opacity: .8; }
.gadget-list, .stack-list { padding: .4rem; display: flex; flex-direction: column; gap: .3rem; min-height: 160px; }
.stack-empty { color: #999; font-size: .8rem; text-align: center; padding: .4rem; display: none; }
/* gadget row style */
.gadget-item { display: flex; align-items: center; gap: .4rem; background: #e8eef3;
border: 1px solid #bac4cc; border-radius: 6px; padding: .3rem .5rem; }
.gadget-code { font-family: ui-monospace, monospace; font-size: .8rem; color: #1d3557; flex: 1; }
.gadget-desc { font-size: .75rem; color: #555; }
.gadget-btn { font-size: .75rem; padding: .15rem .4rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 4px; cursor: pointer; white-space: nowrap; }
/* stack row styles — executed (green), current (yellow highlight) */
.stack-item { display: flex; align-items: center; gap: .4rem; background: #fff3cd;
border: 1px solid #f0c040; border-radius: 6px; padding: .3rem .5rem; }
.stack-addr { font-family: ui-monospace, monospace; font-size: .75rem; color: #888; min-width: 56px; }
.stack-code { font-family: ui-monospace, monospace; font-size: .8rem; color: #333; flex: 1; }
.stack-rem { font-size: .75rem; padding: .1rem .35rem; border: 1px solid #c92f3c;
background: transparent; color: #c92f3c; border-radius: 4px; cursor: pointer; }
.stack-item.executed { background: #d4edda; border-color: #6dbd8a; }
.stack-item.current { background: #ffeeba; border-color: #e0a800; box-shadow: 0 0 0 2px #e0a800; }
/* register state bar */
.state-bar { display: flex; align-items: baseline; gap: .5rem; margin-bottom: .4rem;
padding: .4rem .7rem; background: #f8f9fa; border: 1px solid #dee2e6; border-radius: 6px; }
.state-label { font-weight: 700; color: #1d3557; white-space: nowrap; }
.reg-display { font-family: ui-monospace, monospace; font-size: .8rem; color: #333; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info { color: #1d3557; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Available gadgets — each ends with ret, transferring control to the next address on the stack.
// In a real attack these come from existing library code; here they are simplified for clarity.
const GADGETS = [
{ id: 'pop_eax', code: 'pop eax ; ret', effect: s => { s.eax = 1; }, desc: 'sets eax = 1' },
{ id: 'pop_ebx', code: 'pop ebx ; ret', effect: s => { s.ebx = 2; }, desc: 'sets ebx = 2' },
{ id: 'add_eax', code: 'add eax, ebx ; ret', effect: s => { s.eax += s.ebx; }, desc: 'eax = eax + ebx' },
{ id: 'mov_mem', code: 'mov [mem], eax ; ret', effect: s => { s.mem = s.eax; }, desc: 'writes eax into memory' },
{ id: 'xor_eax', code: 'xor eax, eax ; ret', effect: s => { s.eax = 0; }, desc: 'zeroes eax' },
];
// Goal: reach eax=3 and mem=3 using only gadgets (pop eax, pop ebx, add, then mov).
const GOAL = { eax: 3, mem: 3 };
let chain = [];
let state = { eax: 0, ebx: 0, mem: null };
let executed = 0;
function goalText() {
return 'eax = ' + GOAL.eax + ', mem = ' + GOAL.mem;
}
function regText(s) {
return 'eax=' + s.eax + ' ebx=' + s.ebx + ' [mem]=' + (s.mem === null ? '?' : s.mem);
}
function goalMet(s) {
return s.eax === GOAL.eax && s.mem === GOAL.mem;
}
function renderGadgets() {
const el = document.getElementById('gadget-list');
el.innerHTML = '';
GADGETS.forEach(g => {
const row = document.createElement('div');
row.className = 'gadget-item';
row.innerHTML =
'<span class="gadget-code">' + g.code + '</span>' +
'<span class="gadget-desc">' + g.desc + '</span>' +
'<button class="gadget-btn" data-id="' + g.id + '">Add</button>';
el.appendChild(row);
});
el.querySelectorAll('.gadget-btn').forEach(btn => {
btn.addEventListener('click', () => {
const g = GADGETS.find(x => x.id === btn.dataset.id);
if (g) { chain.push(g); executed = 0; state = { eax: 0, ebx: 0, mem: null }; renderStack(); renderRegs(); setStatus(''); }
});
});
}
function renderStack() {
const el = document.getElementById('stack-list');
const empty = document.getElementById('stack-empty');
el.innerHTML = '';
if (chain.length === 0) { empty.style.display = 'block'; return; }
empty.style.display = 'none';
// render each chain entry with address, code, and a remove button
chain.forEach((g, i) => {
const addr = '0x' + (0xbfff0000 + i * 4).toString(16).toUpperCase();
const row = document.createElement('div');
let cls = 'stack-item';
if (i < executed) cls += ' executed';
else if (i === executed && executed < chain.length) cls += ' current';
row.className = cls;
row.innerHTML =
'<span class="stack-addr">' + addr + '</span>' +
'<span class="stack-code">' + g.code + '</span>' +
'<button class="stack-rem" data-idx="' + i + '">✕</button>';
el.appendChild(row);
});
el.querySelectorAll('.stack-rem').forEach(btn => {
btn.addEventListener('click', () => {
const idx = parseInt(btn.dataset.idx, 10);
chain.splice(idx, 1);
executed = 0; state = { eax: 0, ebx: 0, mem: null };
renderStack(); renderRegs(); setStatus('');
});
});
}
function renderRegs() {
document.getElementById('reg-display').textContent = regText(state);
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg; el.className = 'status ' + (cls || '');
}
function runChain() {
if (chain.length === 0) { setStatus('Add gadgets to the stack first.', 'bad'); return; }
// execute each gadget in order, animating one step at a time
state = { eax: 0, ebx: 0, mem: null };
executed = 0;
renderStack(); renderRegs();
const step = () => {
if (executed >= chain.length) {
if (goalMet(state)) {
setStatus(i18nFmt('Goal reached in %n% gadget(s)! No new code was injected.', { n: chain.length }), 'ok');
} else {
setStatus('Chain finished but goal not met — try a different sequence.', 'bad');
}
renderStack();
return;
}
const g = chain[executed];
g.effect(state);
executed++;
renderRegs();
renderStack();
setStatus(i18nFmt('Executing gadget %step% / %total%: %code%', { step: executed, total: chain.length, code: g.code }), 'info');
setTimeout(step, 420);
};
step();
}
document.getElementById('btn-run').addEventListener('click', runChain);
document.getElementById('btn-reset').addEventListener('click', () => {
chain = []; executed = 0; state = { eax: 0, ebx: 0, mem: null };
renderStack(); renderRegs(); setStatus('');
});
document.getElementById('goal-text').textContent = goalText();
renderGadgets();
renderStack();
renderRegs();
Notice: each gadget does one tiny thing. Chained together they accomplish something that would require real code. The program never left its own executable pages — the attacker only controlled which existing instructions ran and in what order.
Comments
Loading comments...