零、目的
写这篇文章的目的是为了更好地去理解堆,不要只停留在理论知识阶段
注:读这篇文章需要一定的堆基础,可以先去下面的参考链接学一些基础知识,也可以自行找一些堆相关文章。
参考链接:ctf-wiki
一、调试环境
ubuntu16.04
参考链接:ubuntu16.04安装教程(需翻墙访问,去年写的教程,不知道还能不能用)
二、源C代码&编译指令
//源C代码 cs1g.c
//gcc -g cs1g.c -o cs1g
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
int i;
void *xy1,*xy2,*xy3,*xy4,*xy5,*xy6; //0之前vmmap指令查看不到heap堆
xy1 = malloc(0x30);
xy2 = malloc(0x30);
xy3 = malloc(0x30);
free(xy1);
*(long long *)((long long)xy2) = 0x112233445566778899121314151617181920;
*(long long *)((long long)xy3) = 0x55667788;
xy4 = malloc(0x30);
xy5 = malloc(0x30);
free(xy2);
xy6 = malloc(0x30);
free(xy3);
free(xy4);
free(xy5);
free(xy6);
return 0;
}
三、一些调试指令
pwndbg> top_chunk #打印top chunk初始地址
pwndbg> vmmap #查看vmmap表
pwndbg> bin #查看bin表
pwndbg> heap #查看堆
pwndbg> file ./<二进制文件名> #导入源代码,方便调试
pwndbg> x/150gx 0x<地址> #打印堆信息 x/150gx 0x555555559000
pwndbg> x/150xb 0x<地址>
四、调试过程
1、目前还未新建堆
2、新建一个大小为0x30的堆块
3、新建三个大小为0x30的堆块
4、free(xy1); free操作,xy2、xy3写入数据
5、 chunk xy4
6、chunk xy5
7、free(xy2);
8、chunk xy6
9、free(xy3-xy6); ,bin中出现4个大小为0x40的空闲堆块
按先后顺序依次free xy3-xy6 ,则 fastbin 中chunk逻辑连接顺序为xy6->xy5->xy4->xy3
物理连接:内存地址上相互相邻
逻辑连接:由指针前后相连,内存地址上可以不相邻
注:仅给出调试过程与部分知识,其他有关堆基础的知识请自行去别的地方学
其他:
//gcc -g xxx.c -o xxx
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
int i,num;
num = 0x90;
void *xy1,*xy2,*xy3,*xy4,*xy5,*xy6; //0之前vmmap指令查看不到heap堆
xy1 = malloc(num);
xy2 = malloc(num);
xy3 = malloc(num);
free(xy1);
*(long long *)((long long)xy2) = 0x112233445566778899121314151617181920;
*(long long *)((long long)xy3) = 0x55667788;
xy4 = malloc(num);
xy5 = malloc(num);
free(xy2);
xy6 = malloc(num);
free(xy3);
free(xy4);
free(xy5);
free(xy6);
return 0;
}
上面的代码中执行free()时相邻的chunk会被合并所以不太直观
//gcc -g xxx.c -o xxx
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
int i,num;
num = 0x90; //修改num值用于观察small bin、large bin等
void *xy1,*xy2,*xy3,*xy4,*xy5,*xy6,*xy7,*xy8; //0之前vmmap指令查看不到heap堆
xy1 = malloc(num);
xy2 = malloc(num);
xy3 = malloc(num);
xy4 = malloc(num);
xy5 = malloc(num);
xy6 = malloc(num);
xy7 = malloc(num);
xy8 = malloc(num);
free(xy2);
free(xy4);
free(xy6);
free(xy8);
return 0;
}
下图中我们可以看到xy8与top chunk合并了,xy8的size为top chunk的size+xy8的size
同时xy2->xy4->xy6,可以观察他们的fd和bk(图中的fk改成fd)
chunk未被分配时:
fd指向前一个空闲chunk(指链表中的前一个,物理地址可能不相邻)
bk指向后一个空闲chunk
将前一个代码中的num修改为0x420, 触发的还是unsortedbin,这不是我想要的结果...
参考链接:large bin
ok,参考以上链接,我们写个新代码试试能不能触发
//gcc -g xxx.c -o xxx
#include<stdio.h>
#include<stdlib.h>
int main(void)
{
int i,num;
num = 0x420; //修改num值用于观察small bin、large bin等
void *xy0,*xy1,*xy2,*xy3,*xy4,*xy5,*xy6,*xy7,*xy8; //0之前vmmap指令查看不到heap堆
xy0 = malloc(num);
xy1 = malloc(num-0x20);
xy2 = malloc(num+0x20);
free(xy1); //占据unsortedbin
xy3 = malloc(num+0x40);
xy4 = malloc(num+0x60);
free(xy3);
xy5 = malloc(num+0x80);
xy6 = malloc(num+0x100);
free(xy5);
xy7 = malloc(num+0x120);
xy8 = malloc(num+0x140);
free(xy7);
return 0;
}
ok,成功触发large bin
因为每个chunk大小不一致,所以large bin并没有形成链,所以每个被free的chunk的fd和bk都指向自身,这句话只是对结果的分析不知道对不对(应该是不对的,请自行找相关文章了解),我对堆的了解还比较少,有兴趣的可以自行修改代码调试
夹在large bin的size和fd、bk之间的应该就是fd_nextsize与bk_nextsize
总结:
参考链接:堆相关数据结构
参考链接:堆
1、unsortedbin 只能存同一大小的多个空闲堆块(被free的chunk)
2、fast bin 最大存储大小为 0x80 的空闲堆块, fastbin 最多可以支持的 bin 的个数为 10 个(我目前的理解是能有10条链,unsortedbin只能有一条链)
3、small bins 中一共有 62 个循环双向链表,每个链表中存储的 chunk 大小都一致
4、large bins 中一共包括 63 个 bin,每个 bin 中的 chunk 的大小不一致
5、前向合并&后向合并
6、..........
笔记就做到这,主要目的是供自己复习。
五、use after free(UAF)
参考链接:UAF
参考链接:ctf-wiki
例1 引用参考链接中的代码
//gcc -g uaf.c -o uaf -m32
#include <stdio.h>
#include <cstdlib>
#include <string.h>
int main()
{
char *p1;
p1 = (char *) malloc(sizeof(char)*10);//申请内存空间
memcpy(p1,"hello",10);
printf("p1 addr:%x,%s\n",p1,p1);
free(p1);//释放内存空间
char *p2;
p2 = (char *)malloc(sizeof(char)*10);//二次申请内存空间,与第一次大小相同,申请到了同一块内存
memcpy(p1,"world",10);//对内存进行修改
printf("p2 addr:%x,%s\n",p2,p1);//验证
return 0;
}
通过以上代码我对 UAF 理解是:以为glibc的堆回收机制free(p1)后并没有真正释放掉p1,而是把堆块放进了 bin 的 fastbin 中,所以p1指针还是指向了原堆块的物理地址(内存地址,或者说p1的值没变成空),当p2新申请一块堆块时,因为大小与原堆块一致,所以把bin中p1申请的堆块给了p2(也就是把p2的值变为原堆块的物理地址),这是指向原堆块的指针就有了p1、p2,所以p1、p2都可以修改原堆块的数据。
这例子感觉没啥好调的,这里就不调试了。
例2 修改自参考链接中的代码
//源C代码 cs1i.c
//gcc -g -z norelro -no-pie -z execstack -fno-stack-protector -o cs1i cs1i.c -m32
//gcc -g -o cs1i cs1i.c -m32
#include <stdio.h>
#include <stdlib.h>
typedef void (*func_ptr)(char *);
void evil_fuc(char command[])
{
system(command);
}
void echo(char content[])
{
printf("%s",content);
}
int main()
{
int a=1;
func_ptr *p1=(func_ptr*)malloc(4*sizeof(int));
printf("malloc addr: %p\n",p1);
p1[a]=echo;
printf("p1=%p &p1=%p *p1=%p p1[0]=%p &p1[0]=%p p1[a]=%p &p1[a]=%p\n",p1,&p1,*p1,p1[0],&p1[0],p1[a],&p1[a]);
p1[a]("hello world\n");
free(p1); //在这里free了p1,但并未将p1置空,导致后续可以再使用p1指针
p1[a]("hello again\n"); //p1指针未被置空,虽然free了,但仍可使用.
func_ptr *p2=(func_ptr*)malloc(4*sizeof(int));//malloc在free一块内存后,再次申请同样大小的指针会把刚刚释放的内存分配出来.
printf("malloc addr: %p\n",p2);
printf("malloc addr: %p\n",p1);//p2与p1指针指向的内存为同一地址
p2[a]=evil_fuc; //在这里将p1指针里面保存的echo函数指针覆盖成为了evil_func指针.
p1[a]("/bin/sh");
return 0;
}
运行结果如下:
①:*p1为空是因为堆上没有数据,全0
②:p1[1]是指p1存储的堆地址往后4*8=32个字节,同理p1[2]是指p1往后64个字节,同时p1、&p1[0]、&p1[1]、&p1[2]都是堆地址
③:p1[a] = echo; 是指把echo函数的地址写入&p1[a]这一地址的存储单元中,p1[q]就是这一存储单元,同时也是我们申请的堆存储单元(堆空间、堆),总之就是echo的函数地址被写入到了我们申请的堆空间中的某个位置。从上面的p1[a]=0x8048492可知echo函数的地址为0x8048492
通过readelf -a cs1i就可以查找echo函数的地址
这例子我感觉也没啥好调的,有兴趣的可以自己调调看。