OFF-BY-ONE 例题分析
npuctf_2020_easyheap
题目提示 ubuntu18.04 版本,高版本 libc 2.27 tcachebin attack( tcache poisoning )
1 | int __fastcall main(int argc, const char **argv, const char **envp) |
create():
1 | unsigned __int64 create() |
这里就是创建一个新的堆,可以输入大小和内容,并且要求大小必须是 24 或者 56
edit():
1 | unsigned __int64 edit() |
第一个参数:(_QWORD )(((_QWORD )&heaparray + n0xA)+8LL)相当于*(heaparray[n0xA]+8)
第二个参数是第 n0xA 号堆块地址处存储的数值 +1,这个值就是 read_input 要读取的输入长度。
这里存在 off-by-one 漏洞。
show():
1 | unsigned __int64 show() |
输出写入的堆块的 content 地址(ptr+8)(_QWORD )(((_QWORD )&heaparray + n0xA)+8LL),
输出写入长度 → 堆块的 size + 1**((_QWORD **)&heaparray + n0xA)+1LL
delete():
1 | unsigned __int64 delete() |
删除堆块,free 之后指针置 0,不存在 uaf 漏洞。python
exp 基本思路:
构造基本函数:
1 | from pwn import * |
创建三个堆块:打个断点看一下堆情况
1 | add(0x18,'pppp') # chunk0 |
在每一次创建内容堆之前都会创建一个大小为 0x20 的堆。之后我们需要了解一下堆对齐原则:
glibc 堆的内存对齐规则:
- 64 位系统下,堆块的大小必须是 16 字节对齐(即 size 是 16 的倍数)。
- 每个堆块还包含 8 字节的 chunk 头(存储 size、标志位等),所以实际分配的 size = 申请大小 + 8(chunk 头),再向上对齐到 16 的倍数。
malloc(0x10)→ 0x10 + 0x8 = 0x18 → 对齐到 0x20;malloc(0x18)→ 0x18 + 0x8 = 0x20 → 刚好对齐到 0x20。
也就是虽然我们分配的的 0x18 大小的堆,不过由于堆对齐原则,导致当前堆实际大小只有 0x10
1 | pwndbg> x/50gx 0x3aa0c250 |
6 次的 malloc 我们就先认为是创建了 chunk0~chunk5 吧
实现堆溢出,修改 chunk1 的 size 位:
edit(0,payload1)
1 | payload1 = b'a'*0x18 + b'\x41' |
1 | pwndbg> x/50gx 0x4e66250 |
在执行 delete(1) 前,你已经通过 edit(0,'a'*0x18+'\x41') 篡改了原先 chunk2 的 size 第 0 字节,此时:
- 认为 chunk2 的大小为 0x40,1 表示正在使用,向下延伸 0x30 大小的全部变成 chunk2 的 data 区域
add(0x38,payload2)
1 | pwndbg> x/50gx 0x7e25250 |
前面全部作为填充,保证在 data 的最后存放的是 free@got 表地址,方便 show(1)打印出来地址。
寻找 libc 基地址:
1 | show(1) |
打印出来 free@got 地址,通过 libc 计算偏移找到 system 函数地址
调用 system(“/bin/sh”)获取 shell
1 | edit(1,p64(system_addr)) |
1 | pwndbg> x/50gx 0x7c0250 |
0x1b35e300 就是 0x07c0300 的位置,重新调试了一下地址会变化。
最终成功获取 flag
1 | from pwn import * |
更新: 2026-04-23 16:04:52
原文: https://www.yuque.com/idcm/wnemg9/qe5aq4ay2x1meow5