本质:将rbp/rsp迁移至其它地方的一种手段使用指令:
leave、pop rbp
目的:
①可以与输入函数搭配使用,实现任意地址写
②变相增加溢出长度。
①可以与输入函数搭配使用,实现任意地址写
demo1
以下是一个例题:
1 | // gcc -z execstack -fno-stack-protector -no-pie -z norelro -o demo6 demo6.c |
编译一下即可(linux环境)


这里是主函数和test函数,思路是利用栈迁移的办法,把rbp转移到passwd+0xC(当然这里的0xC根据不同的编译环境会有变化的)的地址,此时rbp+var_C=passwd+0xC-0xC=passwd;然后再执行main函数,这样一来x和passwd指向的是同一个地址,也就是说下面的if判断其实指向的是同一个值。(具体动调可以参考上面的视频)
这里我们可以看看ida的汇编代码:

为什么是**passwd_addr+0xC**?
原因是实际赋值的地址是rbp+var_C,而var_C= dword ptr - 0xCh
也就是说需要让**passwd_addr**加上**0xCh**之后再减回去才能做到给passwd赋值
dword ptr是汇编语言里的“操作数大小前缀”,用来告诉 CPU:“我这次要访问的内存单元是 4 字节(double-word)。”
以下下是exp:

py中动态调试:
gdb.attach(io)
②变相增加溢出长度。
正常情况下做栈溢出的题(扫盲)


扫盲:传参顺序rdi,rsi,rdx,rcx,r8,r9

1 | 低地址 ← 栈顶 (RSP指向这里) |
1 | //在程序目录下用该指令编译:gcc -z execstack -fno-stack-protector -no-pie -z norelro -O0 demo2.c |

逻辑是:
先溢出(可以覆盖0x10(dec=16)个字节,先覆盖保存的rbp(即旧的rbp,可以理解成caller的基地址),再覆盖返回地址),修改返回地址(就是被覆盖之后他还是会接着执行剩下的汇编指令的)

把retn换成覆盖成leave,也就是执行两次;然后再在栈上放上对应的rdi、/bin/sh、backdoor即可。
重点是**执行两次leave**
这里我让gemini写了一个html动画
html动画
1 |
|
</head>
<body class="bg-gray-100 min-h-screen p-4 md:p-8 font-sans text-gray-800">
<div class="max-w-6xl mx-auto bg-white rounded-xl shadow-2xl p-6 border border-gray-200">
<h1 class="text-2xl font-bold mb-6 text-gray-800 border-b pb-4 text-center">栈溢出:Stack Pivot (Leave-Ret) 深度演示</h1>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-8">
<!-- 状态面板 -->
<div class="lg:col-span-4 space-y-4">
<div class="bg-gray-50 p-5 rounded-xl border border-gray-200 shadow-sm">
<h2 class="font-bold mb-4 text-xs uppercase tracking-wider text-gray-500">寄存器状态</h2>
<div class="space-y-3 font-mono text-lg">
<div class="flex justify-between items-center border-b border-gray-100 pb-2">
<span class="text-gray-400 text-sm">RSP</span>
<span id="reg-rsp" class="font-bold text-red-600 bg-red-50 px-2 rounded">0x??</span>
</div>
<div class="flex justify-between items-center border-b border-gray-100 pb-2">
<span class="text-gray-400 text-sm">RBP</span>
<span id="reg-rbp" class="font-bold text-blue-600 bg-blue-50 px-2 rounded">0x??</span>
</div>
<div class="flex justify-between items-center border-b border-gray-100 pb-2">
<span class="text-gray-400 text-sm">RIP</span>
<span id="reg-rip" class="font-bold text-green-600 bg-green-50 px-2 rounded">0x??</span>
</div>
</div>
</div>
<div class="bg-gray-900 p-5 rounded-xl shadow-inner">
<h2 class="font-bold mb-2 text-xs uppercase tracking-wider text-gray-500">当前原子指令</h2>
<div id="current-instr" class="text-green-400 font-mono text-xl min-h-[48px] flex items-center">
-
</div>
</div>
<div class="grid grid-cols-2 gap-3">
<button onclick="prevStep()" id="prev-btn" class="bg-white border-2 border-gray-200 hover:border-gray-400 text-gray-700 font-bold py-3 px-4 rounded-lg transition disabled:opacity-30">
上一步
</button>
<button onclick="nextStep()" id="next-btn" class="bg-blue-600 hover:bg-blue-700 text-white font-bold py-3 px-4 rounded-lg shadow-md transition">
下一步
</button>
<button onclick="reset()" class="col-span-2 border border-gray-300 hover:bg-100 text-gray-500 font-medium py-2 px-4 rounded-lg transition text-sm">
重置演示
</button>
</div>
<div class="p-5 bg-amber-50 rounded-xl text-sm text-amber-900 leading-relaxed border border-amber-100 shadow-sm min-h-[150px]">
<div class="flex items-center font-bold mb-2">
<svg class="w-4 h-4 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"></path></svg>
详细步骤说明
</div>
<p id="step-description">点击“下一步”开始。</p>
</div>
</div>
<!-- 栈内存展示 -->
<div class="lg:col-span-8">
<div class="flex font-bold text-gray-400 text-[10px] uppercase tracking-widest mb-3 px-4">
<div class="w-20">地址</div>
<div class="flex-1 text-center">内存数据 (Value)</div>
<div class="w-40 text-right">备注</div>
</div>
<div id="stack-container" class="border-2 border-gray-300 rounded-xl overflow-hidden relative shadow-lg bg-white">
<!-- 动态生成的栈行 -->
</div>
</div>
</div>
</div>
<script>
const initialStack = [
{ addr: '0xa1', val: '0x1111', note: 'New RBP Content', id: 'm-a1', hidden: true },
{ addr: '0xa9', val: 'pop_rdi', note: 'ROP Gadget 1', id: 'm-a9', hidden: true },
{ addr: '0xb1', val: '0xc8', note: 'Arg: /bin/sh addr', id: 'm-b1', hidden: true },
{ addr: '0xb9', val: 'ret', note: 'Stack Align', id: 'm-b9', hidden: true },
{ addr: '0xc1', val: 'backdoor', note: 'Target Func', id: 'm-c1', hidden: true },
{ addr: '...', val: '...', note: '', id: 'gap1', hidden: false },
{ addr: '0xc8', val: '/bin/sh', note: 'String', id: 'm-c8', hidden: false },
{ addr: '...', val: '...', note: '', id: 'gap2', hidden: false },
{ addr: '0xd1', val: '0xe1', note: 'Saved RBP', id: 'm-ebp-loc', hidden: false },
{ addr: '0xd9', val: 'leave_ret', note: 'Return Addr', id: 'm-ret-loc', hidden: false }
];
let currentStepIndex = 0;
const steps = [
{
instr: "Initial State",
desc: "正常栈布局。RBP指向0xd1。此时攻击者已溢出缓冲区,准备修改返回地址。",
rsp: '0xa1', rbp: '0xd1', rip: '0x1000',
action: (data) => {}
},
{
instr: "Overflow!",
desc: "缓冲区溢出发生:0xd1处的旧RBP被覆盖为0xa1,0xd9处的返回地址被覆盖为leave_ret gadget的地址。",
rsp: '0xa1', rbp: '0xd1', rip: '0x1000',
action: (data) => {
data.find(i => i.addr === '0xd1').val = '0xa1';
data.find(i => i.addr === '0xd1').highlight = true;
['0xa1', '0xa9', '0xb1', '0xb9', '0xc1'].forEach(a => data.find(i => i.addr === a).hidden = false);
}
},
{
instr: "leave (1/2): mov rsp, rbp",
desc: "函数正常结束,开始执行自身的leave指令。第一步:将RBP的值赋给RSP。RSP移至0xd1。",
rsp: '0xd1', rbp: '0xd1', rip: 'leave',
action: (data) => {
data.find(i => i.addr === '0xd1').highlight = true;
}
},
{
instr: "leave (2/2): pop rbp",
desc: "leave指令第二步:pop rbp。从栈顶(0xd1)弹出值到RBP。RBP变为0xa1,RSP步进到0xd9。注意:RBP已被劫持!",
rsp: '0xd9', rbp: '0xa1', rip: 'leave',
action: (data) => {}
},
{
instr: "ret (1/2): pop rip",
desc: "执行ret指令。逻辑上等同于 pop rip。从栈顶(0xd9)取出劫持后的返回地址(leave_ret)。",
rsp: '0xd9', rbp: '0xa1', rip: 'ret',
action: (data) => {
data.find(i => i.addr === '0xd9').active = true;
}
},
{
instr: "ret (2/2): jmp rip",
desc: "ret完成。RSP + 8 移动到 0xe1。程序跳转到 gadget 地址执行:这导致程序第二次进入 leave 指令逻辑。",
rsp: '0xe1', rbp: '0xa1', rip: 'leave_ret',
action: (data) => {}
},
{
instr: "leave (1/2): mov rsp, rbp",
desc: "第二次执行leave。第一步:rsp = rbp。由于RBP已经是0xa1,RSP瞬间跳回了低地址的攻击者控制区!",
rsp: '0xa1', rbp: '0xa1', rip: 'leave',
action: (data) => {}
},
{
instr: "leave (2/2): pop rbp",
desc: "第二次执行leave。第二步:pop rbp。从0xa1弹出值。RBP变为0x1111。RSP步进到0xa9。",
rsp: '0xa9', rbp: '0x1111', rip: 'leave',
action: (data) => {}
},
{
instr: "ret: pop rip (ROP Start)",
desc: "第二次leave后的ret动作。从0xa9弹出 pop_rdi 地址到RIP。ROP链正式开始执行。",
rsp: '0xb1', rbp: '0x1111', rip: 'pop_rdi',
action: (data) => {
data.find(i => i.addr === '0xa9').active = true;
}
},
{
instr: "pop rdi; ret",
desc: "pop rdi将0xc8弹出到RDI寄存器。随后ret指令将RIP设置为backdoor地址。RSP移至0xc1。",
rsp: '0xc1', rbp: '0x1111', rip: 'backdoor',
action: (data) => {
data.find(i => i.addr === '0xb1').active = true;
}
},
{
instr: "Exploit Success!",
desc: "程序跳转到 backdoor 执行,且 RDI 已通过 ROP 链指向了 /bin/sh 字符串。攻击完成。",
rsp: '0xc9', rbp: '0x1111', rip: 'backdoor',
action: (data) => {
data.find(i => i.addr === '0xc1').highlight = true;
}
}
];
function renderStack(data, rspAddr, rbpAddr, ripAddr) {
const container = document.getElementById('stack-container');
container.innerHTML = '';
data.forEach(item => {
const row = document.createElement('div');
row.className = `memory-row flex items-center border-b last:border-0`;
const isRsp = item.addr === rspAddr;
const isRbp = item.addr === rbpAddr;
const isRip = item.addr === ripAddr;
const valClass = item.hidden ? 'hidden-content' : 'revealed-content';
const highlightClass = item.highlight ? 'highlight-change' : '';
const activeClass = item.active ? 'bg-green-50' : '';
row.innerHTML =
<div class="w-20 text-[10px] font-mono text-gray-400 p-2 border-r bg-gray-50 flex items-center justify-center">
${item.addr}
</div>
<div id="${item.id}" class="flex-1 stack-cell font-mono font-bold text-gray-700 ${valClass} ${highlightClass} ${activeClass}">
<span>${item.val}</span>
<div class="pointer-container">
${isRsp ? '<div class="pointer-tag tag-rsp">RSP</div>' : ''}
${isRbp ? '<div class="pointer-tag tag-rbp">RBP</div>' : ''}
${isRip ? '<div class="pointer-tag tag-rip">RIP指向此地址内容</div>' : ''}
</div>
</div>
<div class="w-40 text-[9px] text-gray-400 px-3 italic text-right leading-tight">
${item.note}
</div>
;
container.appendChild(row);
});
document.getElementById('reg-rsp').innerText = '0x' + rspAddr.replace('0x','');
document.getElementById('reg-rbp').innerText = '0x' + rbpAddr.replace('0x','');
document.getElementById('reg-rip').innerText = ripAddr.startsWith('0x') ? ripAddr : 'Exec: ' + ripAddr;
}
function updateUI() {
const state = steps[currentStepIndex];
document.getElementById('current-instr').innerText = state.instr;
document.getElementById('step-description').innerText = state.desc;
let currentData = JSON.parse(JSON.stringify(initialStack));
for (let i = 0; i <= currentStepIndex; i++) {
steps[i].action(currentData);
}
renderStack(currentData, state.rsp, state.rbp, state.rip);
document.getElementById('prev-btn').disabled = currentStepIndex === 0;
document.getElementById('next-btn').innerText = currentStepIndex === steps.length - 1 ? "完成" : "下一步";
document.getElementById('next-btn').disabled = currentStepIndex === steps.length - 1;
}
function nextStep() {
if (currentStepIndex < steps.length - 1) {
currentStepIndex++;
updateUI();
}
}
function prevStep() {
if (currentStepIndex > 0) {
currentStepIndex--;
updateUI();
}
}
function reset() {
currentStepIndex = 0;
updateUI();
}
window.onload = updateUI;
</script>
</body>
</html>
同时引入pwngdb分屏看的方法

好现在来看exp如何写
一开始会打印buffer的地址printf("str1 is %p,input2:", buf);
str1 is **0x7fffffffdd20**,input2:aaaaaaaa←返回的地址长14字节

1 | $ ROPgadget --binary a.out --only "pop|ret" |
1 | from pwn import * |
demo3
1 | //在程序目录下用该指令编译:gcc -z execstack -fno-stack-protector -no-pie -z norelro -O0 demo3.c |
唯一不同在于不打印栈地址了,所以需要把shell写到bss段上:

我们找到了可写地址是0x403000-0x404000,注意一下原本程序到的地址:

稍微距离这里远一点:可以给stack给一个地址0x403600

可以尝试用excel画一下栈的运行逻辑,就应该能知道payload构成了:
1.先利用栈溢出把保存的rbp地址转成stack地址、返回地址转成read函数地址
2.此时rbp已经到stack那边去了,然后留下rsp在原栈;执行leave、return指令之后rsp来到新栈处、rbp来到stack-0x50处,rsp+8
3.stack下一个地址可以用leave_retn指令、也可以用leave:前者的填充栈需要用两个0x1,0x1填充(retn会让rsp+8)、后者一个即可
4.接下来的payload跟原本的差不多了
1 | from pwn import * |